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
.groups
.GroupId
;
22 import org
.asamk
.signal
.manager
.groups
.GroupIdV1
;
23 import org
.asamk
.signal
.manager
.groups
.GroupIdV2
;
24 import org
.asamk
.signal
.manager
.groups
.GroupInviteLinkUrl
;
25 import org
.asamk
.signal
.manager
.groups
.GroupNotFoundException
;
26 import org
.asamk
.signal
.manager
.groups
.GroupUtils
;
27 import org
.asamk
.signal
.manager
.groups
.NotAGroupMemberException
;
28 import org
.asamk
.signal
.manager
.helper
.GroupHelper
;
29 import org
.asamk
.signal
.manager
.helper
.PinHelper
;
30 import org
.asamk
.signal
.manager
.helper
.ProfileHelper
;
31 import org
.asamk
.signal
.manager
.helper
.UnidentifiedAccessHelper
;
32 import org
.asamk
.signal
.manager
.storage
.SignalAccount
;
33 import org
.asamk
.signal
.manager
.storage
.contacts
.ContactInfo
;
34 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfo
;
35 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfoV1
;
36 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfoV2
;
37 import org
.asamk
.signal
.manager
.storage
.profiles
.SignalProfile
;
38 import org
.asamk
.signal
.manager
.storage
.profiles
.SignalProfileEntry
;
39 import org
.asamk
.signal
.manager
.storage
.protocol
.IdentityInfo
;
40 import org
.asamk
.signal
.manager
.storage
.stickers
.Sticker
;
41 import org
.asamk
.signal
.manager
.util
.AttachmentUtils
;
42 import org
.asamk
.signal
.manager
.util
.IOUtils
;
43 import org
.asamk
.signal
.manager
.util
.KeyUtils
;
44 import org
.asamk
.signal
.manager
.util
.MessageCacheUtils
;
45 import org
.asamk
.signal
.manager
.util
.Utils
;
46 import org
.signal
.libsignal
.metadata
.InvalidMetadataMessageException
;
47 import org
.signal
.libsignal
.metadata
.InvalidMetadataVersionException
;
48 import org
.signal
.libsignal
.metadata
.ProtocolDuplicateMessageException
;
49 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyException
;
50 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyIdException
;
51 import org
.signal
.libsignal
.metadata
.ProtocolInvalidMessageException
;
52 import org
.signal
.libsignal
.metadata
.ProtocolInvalidVersionException
;
53 import org
.signal
.libsignal
.metadata
.ProtocolLegacyMessageException
;
54 import org
.signal
.libsignal
.metadata
.ProtocolNoSessionException
;
55 import org
.signal
.libsignal
.metadata
.ProtocolUntrustedIdentityException
;
56 import org
.signal
.libsignal
.metadata
.SelfSendException
;
57 import org
.signal
.libsignal
.metadata
.certificate
.CertificateValidator
;
58 import org
.signal
.storageservice
.protos
.groups
.GroupChange
;
59 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroup
;
60 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroupJoinInfo
;
61 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedMember
;
62 import org
.signal
.zkgroup
.InvalidInputException
;
63 import org
.signal
.zkgroup
.VerificationFailedException
;
64 import org
.signal
.zkgroup
.auth
.AuthCredentialResponse
;
65 import org
.signal
.zkgroup
.groups
.GroupMasterKey
;
66 import org
.signal
.zkgroup
.groups
.GroupSecretParams
;
67 import org
.signal
.zkgroup
.profiles
.ClientZkProfileOperations
;
68 import org
.signal
.zkgroup
.profiles
.ProfileKey
;
69 import org
.signal
.zkgroup
.profiles
.ProfileKeyCredential
;
70 import org
.slf4j
.Logger
;
71 import org
.slf4j
.LoggerFactory
;
72 import org
.whispersystems
.libsignal
.IdentityKey
;
73 import org
.whispersystems
.libsignal
.IdentityKeyPair
;
74 import org
.whispersystems
.libsignal
.InvalidKeyException
;
75 import org
.whispersystems
.libsignal
.InvalidMessageException
;
76 import org
.whispersystems
.libsignal
.InvalidVersionException
;
77 import org
.whispersystems
.libsignal
.ecc
.Curve
;
78 import org
.whispersystems
.libsignal
.ecc
.ECKeyPair
;
79 import org
.whispersystems
.libsignal
.ecc
.ECPublicKey
;
80 import org
.whispersystems
.libsignal
.state
.PreKeyRecord
;
81 import org
.whispersystems
.libsignal
.state
.SignedPreKeyRecord
;
82 import org
.whispersystems
.libsignal
.util
.KeyHelper
;
83 import org
.whispersystems
.libsignal
.util
.Medium
;
84 import org
.whispersystems
.libsignal
.util
.Pair
;
85 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
86 import org
.whispersystems
.signalservice
.api
.KbsPinData
;
87 import org
.whispersystems
.signalservice
.api
.KeyBackupService
;
88 import org
.whispersystems
.signalservice
.api
.KeyBackupServicePinException
;
89 import org
.whispersystems
.signalservice
.api
.KeyBackupSystemNoDataException
;
90 import org
.whispersystems
.signalservice
.api
.SignalServiceAccountManager
;
91 import org
.whispersystems
.signalservice
.api
.SignalServiceMessagePipe
;
92 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageReceiver
;
93 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageSender
;
94 import org
.whispersystems
.signalservice
.api
.crypto
.InvalidCiphertextException
;
95 import org
.whispersystems
.signalservice
.api
.crypto
.ProfileCipher
;
96 import org
.whispersystems
.signalservice
.api
.crypto
.SignalServiceCipher
;
97 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccessPair
;
98 import org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException
;
99 import org
.whispersystems
.signalservice
.api
.groupsv2
.ClientZkOperations
;
100 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupLinkNotActiveException
;
101 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Api
;
102 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2AuthorizationString
;
103 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Operations
;
104 import org
.whispersystems
.signalservice
.api
.kbs
.MasterKey
;
105 import org
.whispersystems
.signalservice
.api
.messages
.SendMessageResult
;
106 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachment
;
107 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentPointer
;
108 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentRemoteId
;
109 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentStream
;
110 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceContent
;
111 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceDataMessage
;
112 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceEnvelope
;
113 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroup
;
114 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroupV2
;
115 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceReceiptMessage
;
116 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceStickerManifestUpload
;
117 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceStickerManifestUpload
.StickerInfo
;
118 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.BlockedListMessage
;
119 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ContactsMessage
;
120 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContact
;
121 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsInputStream
;
122 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsOutputStream
;
123 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroup
;
124 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsInputStream
;
125 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsOutputStream
;
126 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceInfo
;
127 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.RequestMessage
;
128 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SentTranscriptMessage
;
129 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SignalServiceSyncMessage
;
130 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.StickerPackOperationMessage
;
131 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.VerifiedMessage
;
132 import org
.whispersystems
.signalservice
.api
.profiles
.ProfileAndCredential
;
133 import org
.whispersystems
.signalservice
.api
.profiles
.SignalServiceProfile
;
134 import org
.whispersystems
.signalservice
.api
.push
.ContactTokenDetails
;
135 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
136 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.MissingConfigurationException
;
137 import org
.whispersystems
.signalservice
.api
.util
.InvalidNumberException
;
138 import org
.whispersystems
.signalservice
.api
.util
.PhoneNumberFormatter
;
139 import org
.whispersystems
.signalservice
.api
.util
.SleepTimer
;
140 import org
.whispersystems
.signalservice
.api
.util
.StreamDetails
;
141 import org
.whispersystems
.signalservice
.api
.util
.UptimeSleepTimer
;
142 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
143 import org
.whispersystems
.signalservice
.internal
.configuration
.SignalServiceConfiguration
;
144 import org
.whispersystems
.signalservice
.internal
.contacts
.crypto
.Quote
;
145 import org
.whispersystems
.signalservice
.internal
.contacts
.crypto
.UnauthenticatedQuoteException
;
146 import org
.whispersystems
.signalservice
.internal
.contacts
.crypto
.UnauthenticatedResponseException
;
147 import org
.whispersystems
.signalservice
.internal
.push
.LockedException
;
148 import org
.whispersystems
.signalservice
.internal
.push
.SignalServiceProtos
;
149 import org
.whispersystems
.signalservice
.internal
.push
.UnsupportedDataMessageException
;
150 import org
.whispersystems
.signalservice
.internal
.push
.VerifyAccountResponse
;
151 import org
.whispersystems
.signalservice
.internal
.util
.DynamicCredentialsProvider
;
152 import org
.whispersystems
.signalservice
.internal
.util
.Hex
;
153 import org
.whispersystems
.util
.Base64
;
155 import java
.io
.Closeable
;
157 import java
.io
.FileInputStream
;
158 import java
.io
.FileNotFoundException
;
159 import java
.io
.FileOutputStream
;
160 import java
.io
.IOException
;
161 import java
.io
.InputStream
;
162 import java
.io
.OutputStream
;
164 import java
.net
.URISyntaxException
;
165 import java
.net
.URLEncoder
;
166 import java
.nio
.charset
.StandardCharsets
;
167 import java
.nio
.file
.Files
;
168 import java
.nio
.file
.Paths
;
169 import java
.nio
.file
.StandardCopyOption
;
170 import java
.security
.KeyStore
;
171 import java
.security
.SignatureException
;
172 import java
.util
.ArrayList
;
173 import java
.util
.Arrays
;
174 import java
.util
.Collection
;
175 import java
.util
.Date
;
176 import java
.util
.HashMap
;
177 import java
.util
.HashSet
;
178 import java
.util
.List
;
179 import java
.util
.Locale
;
180 import java
.util
.Map
;
181 import java
.util
.Objects
;
182 import java
.util
.Set
;
183 import java
.util
.UUID
;
184 import java
.util
.concurrent
.ExecutorService
;
185 import java
.util
.concurrent
.TimeUnit
;
186 import java
.util
.concurrent
.TimeoutException
;
187 import java
.util
.stream
.Collectors
;
188 import java
.util
.zip
.ZipEntry
;
189 import java
.util
.zip
.ZipFile
;
191 import static org
.asamk
.signal
.manager
.ServiceConfig
.CDS_MRENCLAVE
;
192 import static org
.asamk
.signal
.manager
.ServiceConfig
.capabilities
;
193 import static org
.asamk
.signal
.manager
.ServiceConfig
.getIasKeyStore
;
195 public class Manager
implements Closeable
{
197 final static Logger logger
= LoggerFactory
.getLogger(Manager
.class);
199 private final SleepTimer timer
= new UptimeSleepTimer();
200 private final CertificateValidator certificateValidator
= new CertificateValidator(ServiceConfig
.getUnidentifiedSenderTrustRoot());
202 private final SignalServiceConfiguration serviceConfiguration
;
203 private final String userAgent
;
205 // TODO make configurable
206 private final boolean discoverableByPhoneNumber
= true;
207 private final boolean unrestrictedUnidentifiedAccess
= false;
209 private final SignalAccount account
;
210 private final PathConfig pathConfig
;
211 private SignalServiceAccountManager accountManager
;
212 private GroupsV2Api groupsV2Api
;
213 private final GroupsV2Operations groupsV2Operations
;
215 private SignalServiceMessageReceiver messageReceiver
= null;
216 private SignalServiceMessagePipe messagePipe
= null;
217 private SignalServiceMessagePipe unidentifiedMessagePipe
= null;
219 private final UnidentifiedAccessHelper unidentifiedAccessHelper
;
220 private final ProfileHelper profileHelper
;
221 private final GroupHelper groupHelper
;
222 private PinHelper pinHelper
;
225 SignalAccount account
,
226 PathConfig pathConfig
,
227 SignalServiceConfiguration serviceConfiguration
,
230 this.account
= account
;
231 this.pathConfig
= pathConfig
;
232 this.serviceConfiguration
= serviceConfiguration
;
233 this.userAgent
= userAgent
;
234 this.groupsV2Operations
= capabilities
.isGv2() ?
new GroupsV2Operations(ClientZkOperations
.create(
235 serviceConfiguration
)) : null;
236 createSignalServiceAccountManager();
238 this.account
.setResolver(this::resolveSignalServiceAddress
);
240 this.unidentifiedAccessHelper
= new UnidentifiedAccessHelper(account
::getProfileKey
,
241 account
.getProfileStore()::getProfileKey
,
242 this::getRecipientProfile
,
243 this::getSenderCertificate
);
244 this.profileHelper
= new ProfileHelper(account
.getProfileStore()::getProfileKey
,
245 unidentifiedAccessHelper
::getAccessFor
,
246 unidentified
-> unidentified ?
getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(),
247 this::getOrCreateMessageReceiver
);
248 this.groupHelper
= new GroupHelper(this::getRecipientProfileKeyCredential
,
249 this::getRecipientProfile
,
250 account
::getSelfAddress
,
253 this::getGroupAuthForToday
);
256 public String
getUsername() {
257 return account
.getUsername();
260 public SignalServiceAddress
getSelfAddress() {
261 return account
.getSelfAddress();
264 private void createSignalServiceAccountManager() {
265 this.accountManager
= new SignalServiceAccountManager(serviceConfiguration
,
266 new DynamicCredentialsProvider(account
.getUuid(),
267 account
.getUsername(),
268 account
.getPassword(),
270 account
.getDeviceId()),
274 this.groupsV2Api
= accountManager
.getGroupsV2Api();
275 this.pinHelper
= new PinHelper(createKeyBackupService());
278 private KeyBackupService
createKeyBackupService() {
279 KeyStore keyStore
= ServiceConfig
.getIasKeyStore();
281 return accountManager
.getKeyBackupService(keyStore
,
282 ServiceConfig
.KEY_BACKUP_ENCLAVE_NAME
,
283 ServiceConfig
.KEY_BACKUP_SERVICE_ID
,
284 ServiceConfig
.KEY_BACKUP_MRENCLAVE
,
288 private IdentityKeyPair
getIdentityKeyPair() {
289 return account
.getSignalProtocolStore().getIdentityKeyPair();
292 public int getDeviceId() {
293 return account
.getDeviceId();
296 private File
getMessageCachePath() {
297 return SignalAccount
.getMessageCachePath(pathConfig
.getDataPath(), account
.getUsername());
300 private File
getMessageCachePath(String sender
) {
301 if (sender
== null || sender
.isEmpty()) {
302 return getMessageCachePath();
305 return new File(getMessageCachePath(), sender
.replace("/", "_"));
308 private File
getMessageCacheFile(String sender
, long now
, long timestamp
) throws IOException
{
309 File cachePath
= getMessageCachePath(sender
);
310 IOUtils
.createPrivateDirectories(cachePath
);
311 return new File(cachePath
, now
+ "_" + timestamp
);
314 public static Manager
init(
315 String username
, File settingsPath
, SignalServiceConfiguration serviceConfiguration
, String userAgent
316 ) throws IOException
{
317 PathConfig pathConfig
= PathConfig
.createDefault(settingsPath
);
319 if (!SignalAccount
.userExists(pathConfig
.getDataPath(), username
)) {
320 IdentityKeyPair identityKey
= KeyHelper
.generateIdentityKeyPair();
321 int registrationId
= KeyHelper
.generateRegistrationId(false);
323 ProfileKey profileKey
= KeyUtils
.createProfileKey();
324 SignalAccount account
= SignalAccount
.create(pathConfig
.getDataPath(),
331 return new Manager(account
, pathConfig
, serviceConfiguration
, userAgent
);
334 SignalAccount account
= SignalAccount
.load(pathConfig
.getDataPath(), username
);
336 Manager m
= new Manager(account
, pathConfig
, serviceConfiguration
, userAgent
);
338 m
.migrateLegacyConfigs();
343 private void migrateLegacyConfigs() {
344 if (account
.getProfileKey() == null && isRegistered()) {
345 // Old config file, creating new profile key
346 account
.setProfileKey(KeyUtils
.createProfileKey());
349 // Store profile keys only in profile store
350 for (ContactInfo contact
: account
.getContactStore().getContacts()) {
351 String profileKeyString
= contact
.profileKey
;
352 if (profileKeyString
== null) {
355 final ProfileKey profileKey
;
357 profileKey
= new ProfileKey(Base64
.decode(profileKeyString
));
358 } catch (InvalidInputException
| IOException e
) {
361 contact
.profileKey
= null;
362 account
.getProfileStore().storeProfileKey(contact
.getAddress(), profileKey
);
364 // Ensure our profile key is stored in profile store
365 account
.getProfileStore().storeProfileKey(getSelfAddress(), account
.getProfileKey());
368 public void checkAccountState() throws IOException
{
369 if (account
.isRegistered()) {
370 if (accountManager
.getPreKeysCount() < ServiceConfig
.PREKEY_MINIMUM_COUNT
) {
374 if (account
.getUuid() == null) {
375 account
.setUuid(accountManager
.getOwnUuid());
378 updateAccountAttributes();
382 public boolean isRegistered() {
383 return account
.isRegistered();
387 * This is used for checking a set of phone numbers for registration on Signal
389 * @param numbers The set of phone number in question
390 * @return A map of numbers to booleans. True if registered, false otherwise. Should never be null
391 * @throws IOException if its unable to check if the users are registered
393 public Map
<String
, Boolean
> areUsersRegistered(Set
<String
> numbers
) throws IOException
{
394 // Note "contactDetails" has no optionals. It only gives us info on users who are registered
395 List
<ContactTokenDetails
> contactDetails
= this.accountManager
.getContacts(numbers
);
397 // Make the initial map with all numbers set to false for now
398 Map
<String
, Boolean
> usersRegistered
= numbers
.stream().collect(Collectors
.toMap(x
-> x
, x
-> false));
400 // Override the contacts we did obtain
401 for (ContactTokenDetails contactDetail
: contactDetails
) {
402 usersRegistered
.put(contactDetail
.getNumber(), true);
405 return usersRegistered
;
408 public void register(boolean voiceVerification
, String captcha
) throws IOException
{
409 account
.setPassword(KeyUtils
.createPassword());
411 // Resetting UUID, because registering doesn't work otherwise
412 account
.setUuid(null);
413 createSignalServiceAccountManager();
415 if (voiceVerification
) {
416 accountManager
.requestVoiceVerificationCode(Locale
.getDefault(),
417 Optional
.fromNullable(captcha
),
420 accountManager
.requestSmsVerificationCode(false, Optional
.fromNullable(captcha
), Optional
.absent());
423 account
.setRegistered(false);
427 public void updateAccountAttributes() throws IOException
{
428 accountManager
.setAccountAttributes(account
.getSignalingKey(),
429 account
.getSignalProtocolStore().getLocalRegistrationId(),
431 // set legacy pin only if no KBS master key is set
432 account
.getPinMasterKey() == null ? account
.getRegistrationLockPin() : null,
433 account
.getPinMasterKey() == null ?
null : account
.getPinMasterKey().deriveRegistrationLock(),
434 unidentifiedAccessHelper
.getSelfUnidentifiedAccessKey(),
435 unrestrictedUnidentifiedAccess
,
437 discoverableByPhoneNumber
);
440 public void setProfile(String name
, File avatar
) throws IOException
{
441 try (final StreamDetails streamDetails
= avatar
== null ?
null : Utils
.createStreamDetailsFromFile(avatar
)) {
442 accountManager
.setVersionedProfile(account
.getUuid(), account
.getProfileKey(), name
, streamDetails
);
446 public void unregister() throws IOException
{
447 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
448 // If this is the master device, other users can't send messages to this number anymore.
449 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
450 accountManager
.setGcmId(Optional
.absent());
452 account
.setRegistered(false);
456 public List
<DeviceInfo
> getLinkedDevices() throws IOException
{
457 List
<DeviceInfo
> devices
= accountManager
.getDevices();
458 account
.setMultiDevice(devices
.size() > 1);
463 public void removeLinkedDevices(int deviceId
) throws IOException
{
464 accountManager
.removeDevice(deviceId
);
465 List
<DeviceInfo
> devices
= accountManager
.getDevices();
466 account
.setMultiDevice(devices
.size() > 1);
470 public void addDeviceLink(URI linkUri
) throws IOException
, InvalidKeyException
{
471 DeviceLinkInfo info
= DeviceLinkInfo
.parseDeviceLinkUri(linkUri
);
473 addDevice(info
.deviceIdentifier
, info
.deviceKey
);
476 private void addDevice(String deviceIdentifier
, ECPublicKey deviceKey
) throws IOException
, InvalidKeyException
{
477 IdentityKeyPair identityKeyPair
= getIdentityKeyPair();
478 String verificationCode
= accountManager
.getNewDeviceVerificationCode();
480 accountManager
.addDevice(deviceIdentifier
,
483 Optional
.of(account
.getProfileKey().serialize()),
485 account
.setMultiDevice(true);
489 private List
<PreKeyRecord
> generatePreKeys() {
490 List
<PreKeyRecord
> records
= new ArrayList
<>(ServiceConfig
.PREKEY_BATCH_SIZE
);
492 final int offset
= account
.getPreKeyIdOffset();
493 for (int i
= 0; i
< ServiceConfig
.PREKEY_BATCH_SIZE
; i
++) {
494 int preKeyId
= (offset
+ i
) % Medium
.MAX_VALUE
;
495 ECKeyPair keyPair
= Curve
.generateKeyPair();
496 PreKeyRecord
record = new PreKeyRecord(preKeyId
, keyPair
);
501 account
.addPreKeys(records
);
507 private SignedPreKeyRecord
generateSignedPreKey(IdentityKeyPair identityKeyPair
) {
509 ECKeyPair keyPair
= Curve
.generateKeyPair();
510 byte[] signature
= Curve
.calculateSignature(identityKeyPair
.getPrivateKey(),
511 keyPair
.getPublicKey().serialize());
512 SignedPreKeyRecord
record = new SignedPreKeyRecord(account
.getNextSignedPreKeyId(),
513 System
.currentTimeMillis(),
517 account
.addSignedPreKey(record);
521 } catch (InvalidKeyException e
) {
522 throw new AssertionError(e
);
526 public void verifyAccount(
527 String verificationCode
,
529 ) throws IOException
, KeyBackupSystemNoDataException
, KeyBackupServicePinException
{
530 verificationCode
= verificationCode
.replace("-", "");
531 account
.setSignalingKey(KeyUtils
.createSignalingKey());
532 VerifyAccountResponse response
;
534 response
= verifyAccountWithCode(verificationCode
, pin
, null);
535 } catch (LockedException e
) {
540 KbsPinData registrationLockData
= pinHelper
.getRegistrationLockData(pin
, e
);
541 if (registrationLockData
== null) {
545 String registrationLock
= registrationLockData
.getMasterKey().deriveRegistrationLock();
547 response
= verifyAccountWithCode(verificationCode
, null, registrationLock
);
548 } catch (LockedException _e
) {
549 throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
551 account
.setPinMasterKey(registrationLockData
.getMasterKey());
554 // TODO response.isStorageCapable()
555 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
557 account
.setRegistered(true);
558 account
.setUuid(UuidUtil
.parseOrNull(response
.getUuid()));
559 account
.setRegistrationLockPin(pin
);
560 account
.getSignalProtocolStore()
561 .saveIdentity(account
.getSelfAddress(),
562 getIdentityKeyPair().getPublicKey(),
563 TrustLevel
.TRUSTED_VERIFIED
);
569 private VerifyAccountResponse
verifyAccountWithCode(
570 final String verificationCode
, final String legacyPin
, final String registrationLock
571 ) throws IOException
{
572 return accountManager
.verifyAccountWithCode(verificationCode
,
573 account
.getSignalingKey(),
574 account
.getSignalProtocolStore().getLocalRegistrationId(),
578 unidentifiedAccessHelper
.getSelfUnidentifiedAccessKey(),
579 unrestrictedUnidentifiedAccess
,
581 discoverableByPhoneNumber
);
584 public void setRegistrationLockPin(Optional
<String
> pin
) throws IOException
, UnauthenticatedResponseException
{
585 if (pin
.isPresent()) {
586 final MasterKey masterKey
= account
.getPinMasterKey() != null
587 ? account
.getPinMasterKey()
588 : KeyUtils
.createMasterKey();
590 pinHelper
.setRegistrationLockPin(pin
.get(), masterKey
);
592 account
.setRegistrationLockPin(pin
.get());
593 account
.setPinMasterKey(masterKey
);
595 // Remove legacy registration lock
596 accountManager
.removeRegistrationLockV1();
599 pinHelper
.removeRegistrationLockPin();
601 account
.setRegistrationLockPin(null);
602 account
.setPinMasterKey(null);
607 void refreshPreKeys() throws IOException
{
608 List
<PreKeyRecord
> oneTimePreKeys
= generatePreKeys();
609 final IdentityKeyPair identityKeyPair
= getIdentityKeyPair();
610 SignedPreKeyRecord signedPreKeyRecord
= generateSignedPreKey(identityKeyPair
);
612 accountManager
.setPreKeys(identityKeyPair
.getPublicKey(), signedPreKeyRecord
, oneTimePreKeys
);
615 private SignalServiceMessageReceiver
createMessageReceiver() {
616 final ClientZkProfileOperations clientZkProfileOperations
= capabilities
.isGv2() ? ClientZkOperations
.create(
617 serviceConfiguration
).getProfileOperations() : null;
618 return new SignalServiceMessageReceiver(serviceConfiguration
,
620 account
.getUsername(),
621 account
.getPassword(),
622 account
.getDeviceId(),
623 account
.getSignalingKey(),
627 clientZkProfileOperations
);
630 private SignalServiceMessageReceiver
getOrCreateMessageReceiver() {
631 if (messageReceiver
== null) {
632 messageReceiver
= createMessageReceiver();
634 return messageReceiver
;
637 private SignalServiceMessagePipe
getOrCreateMessagePipe() {
638 if (messagePipe
== null) {
639 messagePipe
= getOrCreateMessageReceiver().createMessagePipe();
644 private SignalServiceMessagePipe
getOrCreateUnidentifiedMessagePipe() {
645 if (unidentifiedMessagePipe
== null) {
646 unidentifiedMessagePipe
= getOrCreateMessageReceiver().createUnidentifiedMessagePipe();
648 return unidentifiedMessagePipe
;
651 private SignalServiceMessageSender
createMessageSender() {
652 final ClientZkProfileOperations clientZkProfileOperations
= capabilities
.isGv2() ? ClientZkOperations
.create(
653 serviceConfiguration
).getProfileOperations() : null;
654 final ExecutorService executor
= null;
655 return new SignalServiceMessageSender(serviceConfiguration
,
657 account
.getUsername(),
658 account
.getPassword(),
659 account
.getDeviceId(),
660 account
.getSignalProtocolStore(),
662 account
.isMultiDevice(),
663 Optional
.fromNullable(messagePipe
),
664 Optional
.fromNullable(unidentifiedMessagePipe
),
666 clientZkProfileOperations
,
668 ServiceConfig
.MAX_ENVELOPE_SIZE
);
671 private SignalServiceProfile
getEncryptedRecipientProfile(SignalServiceAddress address
) throws IOException
{
672 return profileHelper
.retrieveProfileSync(address
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
675 private SignalProfile
getRecipientProfile(
676 SignalServiceAddress address
678 SignalProfileEntry profileEntry
= account
.getProfileStore().getProfileEntry(address
);
679 if (profileEntry
== null) {
682 long now
= new Date().getTime();
683 // Profiles are cache for 24h before retrieving them again
684 if (!profileEntry
.isRequestPending() && (
685 profileEntry
.getProfile() == null || now
- profileEntry
.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000
687 ProfileKey profileKey
= profileEntry
.getProfileKey();
688 profileEntry
.setRequestPending(true);
689 SignalProfile profile
;
691 profile
= retrieveRecipientProfile(address
, profileKey
);
692 } catch (IOException e
) {
693 logger
.warn("Failed to retrieve profile, ignoring: {}", e
.getMessage());
694 profileEntry
.setRequestPending(false);
697 profileEntry
.setRequestPending(false);
698 account
.getProfileStore()
699 .updateProfile(address
, profileKey
, now
, profile
, profileEntry
.getProfileKeyCredential());
702 return profileEntry
.getProfile();
705 private ProfileKeyCredential
getRecipientProfileKeyCredential(SignalServiceAddress address
) {
706 SignalProfileEntry profileEntry
= account
.getProfileStore().getProfileEntry(address
);
707 if (profileEntry
== null) {
710 if (profileEntry
.getProfileKeyCredential() == null) {
711 ProfileAndCredential profileAndCredential
;
713 profileAndCredential
= profileHelper
.retrieveProfileSync(address
,
714 SignalServiceProfile
.RequestType
.PROFILE_AND_CREDENTIAL
);
715 } catch (IOException e
) {
716 logger
.warn("Failed to retrieve profile key credential, ignoring: {}", e
.getMessage());
720 long now
= new Date().getTime();
721 final ProfileKeyCredential profileKeyCredential
= profileAndCredential
.getProfileKeyCredential().orNull();
722 final SignalProfile profile
= decryptProfile(address
,
723 profileEntry
.getProfileKey(),
724 profileAndCredential
.getProfile());
725 account
.getProfileStore()
726 .updateProfile(address
, profileEntry
.getProfileKey(), now
, profile
, profileKeyCredential
);
727 return profileKeyCredential
;
729 return profileEntry
.getProfileKeyCredential();
732 private SignalProfile
retrieveRecipientProfile(
733 SignalServiceAddress address
, ProfileKey profileKey
734 ) throws IOException
{
735 final SignalServiceProfile encryptedProfile
= getEncryptedRecipientProfile(address
);
737 return decryptProfile(address
, profileKey
, encryptedProfile
);
740 private SignalProfile
decryptProfile(
741 final SignalServiceAddress address
, final ProfileKey profileKey
, final SignalServiceProfile encryptedProfile
743 File avatarFile
= null;
745 avatarFile
= encryptedProfile
.getAvatar() == null
747 : retrieveProfileAvatar(address
, encryptedProfile
.getAvatar(), profileKey
);
748 } catch (Throwable e
) {
749 logger
.warn("Failed to retrieve profile avatar, ignoring: {}", e
.getMessage());
752 ProfileCipher profileCipher
= new ProfileCipher(profileKey
);
756 name
= encryptedProfile
.getName() == null
758 : new String(profileCipher
.decryptName(Base64
.decode(encryptedProfile
.getName())));
759 } catch (IOException e
) {
762 String unidentifiedAccess
;
764 unidentifiedAccess
= encryptedProfile
.getUnidentifiedAccess() == null
765 || !profileCipher
.verifyUnidentifiedAccess(Base64
.decode(encryptedProfile
.getUnidentifiedAccess()))
767 : encryptedProfile
.getUnidentifiedAccess();
768 } catch (IOException e
) {
769 unidentifiedAccess
= null;
771 return new SignalProfile(encryptedProfile
.getIdentityKey(),
775 encryptedProfile
.isUnrestrictedUnidentifiedAccess(),
776 encryptedProfile
.getCapabilities());
777 } catch (InvalidCiphertextException e
) {
782 private Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(GroupId groupId
) throws IOException
{
783 File file
= getGroupAvatarFile(groupId
);
784 if (!file
.exists()) {
785 return Optional
.absent();
788 return Optional
.of(AttachmentUtils
.createAttachment(file
));
791 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(String number
) throws IOException
{
792 File file
= getContactAvatarFile(number
);
793 if (!file
.exists()) {
794 return Optional
.absent();
797 return Optional
.of(AttachmentUtils
.createAttachment(file
));
800 private GroupInfo
getGroupForSending(GroupId groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
801 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
803 throw new GroupNotFoundException(groupId
);
805 if (!g
.isMember(account
.getSelfAddress())) {
806 throw new NotAGroupMemberException(groupId
, g
.getTitle());
811 private GroupInfo
getGroupForUpdating(GroupId groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
812 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
814 throw new GroupNotFoundException(groupId
);
816 if (!g
.isMember(account
.getSelfAddress()) && !g
.isPendingMember(account
.getSelfAddress())) {
817 throw new NotAGroupMemberException(groupId
, g
.getTitle());
822 public List
<GroupInfo
> getGroups() {
823 return account
.getGroupStore().getGroups();
826 public Pair
<Long
, List
<SendMessageResult
>> sendGroupMessage(
827 SignalServiceDataMessage
.Builder messageBuilder
, GroupId groupId
828 ) throws IOException
, GroupNotFoundException
, NotAGroupMemberException
{
829 final GroupInfo g
= getGroupForSending(groupId
);
831 GroupUtils
.setGroupContext(messageBuilder
, g
);
832 messageBuilder
.withExpiration(g
.getMessageExpirationTime());
834 return sendMessage(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
837 public Pair
<Long
, List
<SendMessageResult
>> sendGroupMessage(
838 String messageText
, List
<String
> attachments
, GroupId groupId
839 ) throws IOException
, GroupNotFoundException
, AttachmentInvalidException
, NotAGroupMemberException
{
840 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
841 .withBody(messageText
);
842 if (attachments
!= null) {
843 messageBuilder
.withAttachments(AttachmentUtils
.getSignalServiceAttachments(attachments
));
846 return sendGroupMessage(messageBuilder
, groupId
);
849 public Pair
<Long
, List
<SendMessageResult
>> sendGroupMessageReaction(
850 String emoji
, boolean remove
, String targetAuthor
, long targetSentTimestamp
, GroupId groupId
851 ) throws IOException
, InvalidNumberException
, NotAGroupMemberException
, GroupNotFoundException
{
852 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
,
854 canonicalizeAndResolveSignalServiceAddress(targetAuthor
),
855 targetSentTimestamp
);
856 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
857 .withReaction(reaction
);
859 return sendGroupMessage(messageBuilder
, groupId
);
862 public Pair
<Long
, List
<SendMessageResult
>> sendQuitGroupMessage(GroupId groupId
) throws GroupNotFoundException
, IOException
, NotAGroupMemberException
{
864 SignalServiceDataMessage
.Builder messageBuilder
;
866 final GroupInfo g
= getGroupForUpdating(groupId
);
867 if (g
instanceof GroupInfoV1
) {
868 GroupInfoV1 groupInfoV1
= (GroupInfoV1
) g
;
869 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
870 .withId(groupId
.serialize())
872 messageBuilder
= SignalServiceDataMessage
.newBuilder().asGroupMessage(group
);
873 groupInfoV1
.removeMember(account
.getSelfAddress());
874 account
.getGroupStore().updateGroup(groupInfoV1
);
876 final GroupInfoV2 groupInfoV2
= (GroupInfoV2
) g
;
877 final Pair
<DecryptedGroup
, GroupChange
> groupGroupChangePair
= groupHelper
.leaveGroup(groupInfoV2
);
878 groupInfoV2
.setGroup(groupGroupChangePair
.first());
879 messageBuilder
= getGroupUpdateMessageBuilder(groupInfoV2
, groupGroupChangePair
.second().toByteArray());
880 account
.getGroupStore().updateGroup(groupInfoV2
);
883 return sendMessage(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
886 private Pair
<GroupId
, List
<SendMessageResult
>> sendUpdateGroupMessage(
887 GroupId groupId
, String name
, Collection
<SignalServiceAddress
> members
, String avatarFile
888 ) throws IOException
, GroupNotFoundException
, AttachmentInvalidException
, NotAGroupMemberException
{
890 SignalServiceDataMessage
.Builder messageBuilder
;
891 if (groupId
== null) {
893 GroupInfoV2 gv2
= groupHelper
.createGroupV2(name
, members
, avatarFile
);
895 GroupInfoV1 gv1
= new GroupInfoV1(GroupIdV1
.createRandom());
896 gv1
.addMembers(List
.of(account
.getSelfAddress()));
897 updateGroupV1(gv1
, name
, members
, avatarFile
);
898 messageBuilder
= getGroupUpdateMessageBuilder(gv1
);
901 messageBuilder
= getGroupUpdateMessageBuilder(gv2
, null);
905 GroupInfo group
= getGroupForUpdating(groupId
);
906 if (group
instanceof GroupInfoV2
) {
907 final GroupInfoV2 groupInfoV2
= (GroupInfoV2
) group
;
909 Pair
<Long
, List
<SendMessageResult
>> result
= null;
910 if (groupInfoV2
.isPendingMember(getSelfAddress())) {
911 Pair
<DecryptedGroup
, GroupChange
> groupGroupChangePair
= groupHelper
.acceptInvite(groupInfoV2
);
912 result
= sendUpdateGroupMessage(groupInfoV2
,
913 groupGroupChangePair
.first(),
914 groupGroupChangePair
.second());
917 if (members
!= null) {
918 final Set
<SignalServiceAddress
> newMembers
= new HashSet
<>(members
);
919 newMembers
.removeAll(group
.getMembers()
921 .map(this::resolveSignalServiceAddress
)
922 .collect(Collectors
.toSet()));
923 if (newMembers
.size() > 0) {
924 Pair
<DecryptedGroup
, GroupChange
> groupGroupChangePair
= groupHelper
.updateGroupV2(groupInfoV2
,
926 result
= sendUpdateGroupMessage(groupInfoV2
,
927 groupGroupChangePair
.first(),
928 groupGroupChangePair
.second());
931 if (result
== null || name
!= null || avatarFile
!= null) {
932 Pair
<DecryptedGroup
, GroupChange
> groupGroupChangePair
= groupHelper
.updateGroupV2(groupInfoV2
,
935 result
= sendUpdateGroupMessage(groupInfoV2
,
936 groupGroupChangePair
.first(),
937 groupGroupChangePair
.second());
940 return new Pair
<>(group
.getGroupId(), result
.second());
942 GroupInfoV1 gv1
= (GroupInfoV1
) group
;
943 updateGroupV1(gv1
, name
, members
, avatarFile
);
944 messageBuilder
= getGroupUpdateMessageBuilder(gv1
);
949 account
.getGroupStore().updateGroup(g
);
951 final Pair
<Long
, List
<SendMessageResult
>> result
= sendMessage(messageBuilder
,
952 g
.getMembersIncludingPendingWithout(account
.getSelfAddress()));
953 return new Pair
<>(g
.getGroupId(), result
.second());
956 public Pair
<GroupId
, List
<SendMessageResult
>> joinGroup(
957 GroupInviteLinkUrl inviteLinkUrl
958 ) throws IOException
, GroupLinkNotActiveException
{
959 return sendJoinGroupMessage(inviteLinkUrl
);
962 private Pair
<GroupId
, List
<SendMessageResult
>> sendJoinGroupMessage(
963 GroupInviteLinkUrl inviteLinkUrl
964 ) throws IOException
, GroupLinkNotActiveException
{
965 final DecryptedGroupJoinInfo groupJoinInfo
= groupHelper
.getDecryptedGroupJoinInfo(inviteLinkUrl
.getGroupMasterKey(),
966 inviteLinkUrl
.getPassword());
967 final GroupChange groupChange
= groupHelper
.joinGroup(inviteLinkUrl
.getGroupMasterKey(),
968 inviteLinkUrl
.getPassword(),
970 final GroupInfoV2 group
= getOrMigrateGroup(inviteLinkUrl
.getGroupMasterKey(),
971 groupJoinInfo
.getRevision() + 1,
972 groupChange
.toByteArray());
974 if (group
.getGroup() == null) {
975 // Only requested member, can't send update to group members
976 return new Pair
<>(group
.getGroupId(), List
.of());
979 final Pair
<Long
, List
<SendMessageResult
>> result
= sendUpdateGroupMessage(group
, group
.getGroup(), groupChange
);
981 return new Pair
<>(group
.getGroupId(), result
.second());
984 private Pair
<Long
, List
<SendMessageResult
>> sendUpdateGroupMessage(
985 GroupInfoV2 group
, DecryptedGroup newDecryptedGroup
, GroupChange groupChange
986 ) throws IOException
{
987 group
.setGroup(newDecryptedGroup
);
988 final SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(group
,
989 groupChange
.toByteArray());
990 account
.getGroupStore().updateGroup(group
);
991 return sendMessage(messageBuilder
, group
.getMembersIncludingPendingWithout(account
.getSelfAddress()));
994 private void updateGroupV1(
997 final Collection
<SignalServiceAddress
> members
,
998 final String avatarFile
999 ) throws IOException
{
1004 if (members
!= null) {
1005 final Set
<String
> newE164Members
= new HashSet
<>();
1006 for (SignalServiceAddress member
: members
) {
1007 if (g
.isMember(member
) || !member
.getNumber().isPresent()) {
1010 newE164Members
.add(member
.getNumber().get());
1013 final List
<ContactTokenDetails
> contacts
= accountManager
.getContacts(newE164Members
);
1014 if (contacts
.size() != newE164Members
.size()) {
1015 // Some of the new members are not registered on Signal
1016 for (ContactTokenDetails contact
: contacts
) {
1017 newE164Members
.remove(contact
.getNumber());
1019 throw new IOException("Failed to add members "
1020 + String
.join(", ", newE164Members
)
1021 + " to group: Not registered on Signal");
1024 g
.addMembers(members
);
1027 if (avatarFile
!= null) {
1028 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
1029 File aFile
= getGroupAvatarFile(g
.getGroupId());
1030 Files
.copy(Paths
.get(avatarFile
), aFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
1034 Pair
<Long
, List
<SendMessageResult
>> sendUpdateGroupMessage(
1035 GroupIdV1 groupId
, SignalServiceAddress recipient
1036 ) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
, AttachmentInvalidException
{
1038 GroupInfo group
= getGroupForSending(groupId
);
1039 if (!(group
instanceof GroupInfoV1
)) {
1040 throw new RuntimeException("Received an invalid group request for a v2 group!");
1042 g
= (GroupInfoV1
) group
;
1044 if (!g
.isMember(recipient
)) {
1045 throw new NotAGroupMemberException(groupId
, g
.name
);
1048 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
1050 // Send group message only to the recipient who requested it
1051 return sendMessage(messageBuilder
, List
.of(recipient
));
1054 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfoV1 g
) throws AttachmentInvalidException
{
1055 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
1056 .withId(g
.getGroupId().serialize())
1058 .withMembers(new ArrayList
<>(g
.getMembers()));
1060 File aFile
= getGroupAvatarFile(g
.getGroupId());
1061 if (aFile
.exists()) {
1063 group
.withAvatar(AttachmentUtils
.createAttachment(aFile
));
1064 } catch (IOException e
) {
1065 throw new AttachmentInvalidException(aFile
.toString(), e
);
1069 return SignalServiceDataMessage
.newBuilder()
1070 .asGroupMessage(group
.build())
1071 .withExpiration(g
.getMessageExpirationTime());
1074 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfoV2 g
, byte[] signedGroupChange
) {
1075 SignalServiceGroupV2
.Builder group
= SignalServiceGroupV2
.newBuilder(g
.getMasterKey())
1076 .withRevision(g
.getGroup().getRevision())
1077 .withSignedGroupChange(signedGroupChange
);
1078 return SignalServiceDataMessage
.newBuilder()
1079 .asGroupMessage(group
.build())
1080 .withExpiration(g
.getMessageExpirationTime());
1083 Pair
<Long
, List
<SendMessageResult
>> sendGroupInfoRequest(
1084 GroupIdV1 groupId
, SignalServiceAddress recipient
1085 ) throws IOException
{
1086 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
)
1087 .withId(groupId
.serialize());
1089 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
1090 .asGroupMessage(group
.build());
1092 // Send group info request message to the recipient who sent us a message with this groupId
1093 return sendMessage(messageBuilder
, List
.of(recipient
));
1097 SignalServiceAddress remoteAddress
, long messageId
1098 ) throws IOException
, UntrustedIdentityException
{
1099 SignalServiceReceiptMessage receiptMessage
= new SignalServiceReceiptMessage(SignalServiceReceiptMessage
.Type
.DELIVERY
,
1101 System
.currentTimeMillis());
1103 createMessageSender().sendReceipt(remoteAddress
,
1104 unidentifiedAccessHelper
.getAccessFor(remoteAddress
),
1108 public Pair
<Long
, List
<SendMessageResult
>> sendMessage(
1109 String messageText
, List
<String
> attachments
, List
<String
> recipients
1110 ) throws IOException
, AttachmentInvalidException
, InvalidNumberException
{
1111 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
1112 .withBody(messageText
);
1113 if (attachments
!= null) {
1114 List
<SignalServiceAttachment
> attachmentStreams
= AttachmentUtils
.getSignalServiceAttachments(attachments
);
1116 // Upload attachments here, so we only upload once even for multiple recipients
1117 SignalServiceMessageSender messageSender
= createMessageSender();
1118 List
<SignalServiceAttachment
> attachmentPointers
= new ArrayList
<>(attachmentStreams
.size());
1119 for (SignalServiceAttachment attachment
: attachmentStreams
) {
1120 if (attachment
.isStream()) {
1121 attachmentPointers
.add(messageSender
.uploadAttachment(attachment
.asStream()));
1122 } else if (attachment
.isPointer()) {
1123 attachmentPointers
.add(attachment
.asPointer());
1127 messageBuilder
.withAttachments(attachmentPointers
);
1129 return sendMessage(messageBuilder
, getSignalServiceAddresses(recipients
));
1132 public Pair
<Long
, List
<SendMessageResult
>> sendMessageReaction(
1133 String emoji
, boolean remove
, String targetAuthor
, long targetSentTimestamp
, List
<String
> recipients
1134 ) throws IOException
, InvalidNumberException
{
1135 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
,
1137 canonicalizeAndResolveSignalServiceAddress(targetAuthor
),
1138 targetSentTimestamp
);
1139 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
1140 .withReaction(reaction
);
1141 return sendMessage(messageBuilder
, getSignalServiceAddresses(recipients
));
1144 public Pair
<Long
, List
<SendMessageResult
>> sendEndSessionMessage(List
<String
> recipients
) throws IOException
, InvalidNumberException
{
1145 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().asEndSessionMessage();
1147 final Collection
<SignalServiceAddress
> signalServiceAddresses
= getSignalServiceAddresses(recipients
);
1149 return sendMessage(messageBuilder
, signalServiceAddresses
);
1150 } catch (Exception e
) {
1151 for (SignalServiceAddress address
: signalServiceAddresses
) {
1152 handleEndSession(address
);
1159 public String
getContactName(String number
) throws InvalidNumberException
{
1160 ContactInfo contact
= account
.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number
));
1161 if (contact
== null) {
1164 return contact
.name
;
1168 public void setContactName(String number
, String name
) throws InvalidNumberException
{
1169 final SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(number
);
1170 ContactInfo contact
= account
.getContactStore().getContact(address
);
1171 if (contact
== null) {
1172 contact
= new ContactInfo(address
);
1174 contact
.name
= name
;
1175 account
.getContactStore().updateContact(contact
);
1179 public void setContactBlocked(String number
, boolean blocked
) throws InvalidNumberException
{
1180 setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number
), blocked
);
1183 private void setContactBlocked(SignalServiceAddress address
, boolean blocked
) {
1184 ContactInfo contact
= account
.getContactStore().getContact(address
);
1185 if (contact
== null) {
1186 contact
= new ContactInfo(address
);
1188 contact
.blocked
= blocked
;
1189 account
.getContactStore().updateContact(contact
);
1193 public void setGroupBlocked(final GroupId groupId
, final boolean blocked
) throws GroupNotFoundException
{
1194 GroupInfo group
= getGroup(groupId
);
1195 if (group
== null) {
1196 throw new GroupNotFoundException(groupId
);
1199 group
.setBlocked(blocked
);
1200 account
.getGroupStore().updateGroup(group
);
1204 public Pair
<GroupId
, List
<SendMessageResult
>> updateGroup(
1205 GroupId groupId
, String name
, List
<String
> members
, String avatar
1206 ) throws IOException
, GroupNotFoundException
, AttachmentInvalidException
, InvalidNumberException
, NotAGroupMemberException
{
1207 return sendUpdateGroupMessage(groupId
,
1209 members
== null ?
null : getSignalServiceAddresses(members
),
1214 * Change the expiration timer for a contact
1216 public void setExpirationTimer(SignalServiceAddress address
, int messageExpirationTimer
) throws IOException
{
1217 ContactInfo contact
= account
.getContactStore().getContact(address
);
1218 contact
.messageExpirationTime
= messageExpirationTimer
;
1219 account
.getContactStore().updateContact(contact
);
1220 sendExpirationTimerUpdate(address
);
1224 private void sendExpirationTimerUpdate(SignalServiceAddress address
) throws IOException
{
1225 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
1226 .asExpirationUpdate();
1227 sendMessage(messageBuilder
, List
.of(address
));
1231 * Change the expiration timer for a contact
1233 public void setExpirationTimer(
1234 String number
, int messageExpirationTimer
1235 ) throws IOException
, InvalidNumberException
{
1236 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(number
);
1237 setExpirationTimer(address
, messageExpirationTimer
);
1241 * Change the expiration timer for a group
1243 public void setExpirationTimer(GroupId groupId
, int messageExpirationTimer
) {
1244 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
1245 if (g
instanceof GroupInfoV1
) {
1246 GroupInfoV1 groupInfoV1
= (GroupInfoV1
) g
;
1247 groupInfoV1
.messageExpirationTime
= messageExpirationTimer
;
1248 account
.getGroupStore().updateGroup(groupInfoV1
);
1250 throw new RuntimeException("TODO Not implemented!");
1255 * Upload the sticker pack from path.
1257 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
1258 * @return if successful, returns the URL to install the sticker pack in the signal app
1260 public String
uploadStickerPack(File path
) throws IOException
, StickerPackInvalidException
{
1261 SignalServiceStickerManifestUpload manifest
= getSignalServiceStickerManifestUpload(path
);
1263 SignalServiceMessageSender messageSender
= createMessageSender();
1265 byte[] packKey
= KeyUtils
.createStickerUploadKey();
1266 String packId
= messageSender
.uploadStickerManifest(manifest
, packKey
);
1268 Sticker sticker
= new Sticker(Hex
.fromStringCondensed(packId
), packKey
);
1269 account
.getStickerStore().updateSticker(sticker
);
1273 return new URI("https",
1276 "pack_id=" + URLEncoder
.encode(packId
, StandardCharsets
.UTF_8
) + "&pack_key=" + URLEncoder
.encode(
1277 Hex
.toStringCondensed(packKey
),
1278 StandardCharsets
.UTF_8
)).toString();
1279 } catch (URISyntaxException e
) {
1280 throw new AssertionError(e
);
1284 private SignalServiceStickerManifestUpload
getSignalServiceStickerManifestUpload(
1286 ) throws IOException
, StickerPackInvalidException
{
1288 String rootPath
= null;
1290 if (file
.getName().endsWith(".zip")) {
1291 zip
= new ZipFile(file
);
1292 } else if (file
.getName().equals("manifest.json")) {
1293 rootPath
= file
.getParent();
1295 throw new StickerPackInvalidException("Could not find manifest.json");
1298 JsonStickerPack pack
= parseStickerPack(rootPath
, zip
);
1300 if (pack
.stickers
== null) {
1301 throw new StickerPackInvalidException("Must set a 'stickers' field.");
1304 if (pack
.stickers
.isEmpty()) {
1305 throw new StickerPackInvalidException("Must include stickers.");
1308 List
<StickerInfo
> stickers
= new ArrayList
<>(pack
.stickers
.size());
1309 for (JsonStickerPack
.JsonSticker sticker
: pack
.stickers
) {
1310 if (sticker
.file
== null) {
1311 throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
1314 Pair
<InputStream
, Long
> data
;
1316 data
= getInputStreamAndLength(rootPath
, zip
, sticker
.file
);
1317 } catch (IOException ignored
) {
1318 throw new StickerPackInvalidException("Could not find find " + sticker
.file
);
1321 String contentType
= Utils
.getFileMimeType(new File(sticker
.file
), null);
1322 StickerInfo stickerInfo
= new StickerInfo(data
.first(),
1324 Optional
.fromNullable(sticker
.emoji
).or(""),
1326 stickers
.add(stickerInfo
);
1329 StickerInfo cover
= null;
1330 if (pack
.cover
!= null) {
1331 if (pack
.cover
.file
== null) {
1332 throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
1335 Pair
<InputStream
, Long
> data
;
1337 data
= getInputStreamAndLength(rootPath
, zip
, pack
.cover
.file
);
1338 } catch (IOException ignored
) {
1339 throw new StickerPackInvalidException("Could not find find " + pack
.cover
.file
);
1342 String contentType
= Utils
.getFileMimeType(new File(pack
.cover
.file
), null);
1343 cover
= new StickerInfo(data
.first(),
1345 Optional
.fromNullable(pack
.cover
.emoji
).or(""),
1349 return new SignalServiceStickerManifestUpload(pack
.title
, pack
.author
, cover
, stickers
);
1352 private static JsonStickerPack
parseStickerPack(String rootPath
, ZipFile zip
) throws IOException
{
1353 InputStream inputStream
;
1355 inputStream
= zip
.getInputStream(zip
.getEntry("manifest.json"));
1357 inputStream
= new FileInputStream((new File(rootPath
, "manifest.json")));
1359 return new ObjectMapper().readValue(inputStream
, JsonStickerPack
.class);
1362 private static Pair
<InputStream
, Long
> getInputStreamAndLength(
1363 final String rootPath
, final ZipFile zip
, final String subfile
1364 ) throws IOException
{
1366 final ZipEntry entry
= zip
.getEntry(subfile
);
1367 return new Pair
<>(zip
.getInputStream(entry
), entry
.getSize());
1369 final File file
= new File(rootPath
, subfile
);
1370 return new Pair
<>(new FileInputStream(file
), file
.length());
1374 void requestSyncGroups() throws IOException
{
1375 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
1376 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
)
1378 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1380 sendSyncMessage(message
);
1381 } catch (UntrustedIdentityException e
) {
1382 e
.printStackTrace();
1386 void requestSyncContacts() throws IOException
{
1387 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
1388 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
)
1390 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1392 sendSyncMessage(message
);
1393 } catch (UntrustedIdentityException e
) {
1394 e
.printStackTrace();
1398 void requestSyncBlocked() throws IOException
{
1399 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
1400 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
)
1402 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1404 sendSyncMessage(message
);
1405 } catch (UntrustedIdentityException e
) {
1406 e
.printStackTrace();
1410 void requestSyncConfiguration() throws IOException
{
1411 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
1412 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
)
1414 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1416 sendSyncMessage(message
);
1417 } catch (UntrustedIdentityException e
) {
1418 e
.printStackTrace();
1422 private byte[] getSenderCertificate() {
1423 // TODO support UUID capable sender certificates
1424 // byte[] certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy();
1427 certificate
= accountManager
.getSenderCertificate();
1428 } catch (IOException e
) {
1429 logger
.warn("Failed to get sender certificate, ignoring: {}", e
.getMessage());
1432 // TODO cache for a day
1436 private void sendSyncMessage(SignalServiceSyncMessage message
) throws IOException
, UntrustedIdentityException
{
1437 SignalServiceMessageSender messageSender
= createMessageSender();
1439 messageSender
.sendMessage(message
, unidentifiedAccessHelper
.getAccessForSync());
1440 } catch (UntrustedIdentityException e
) {
1441 account
.getSignalProtocolStore()
1442 .saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()),
1444 TrustLevel
.UNTRUSTED
);
1449 private Collection
<SignalServiceAddress
> getSignalServiceAddresses(Collection
<String
> numbers
) throws InvalidNumberException
{
1450 final Set
<SignalServiceAddress
> signalServiceAddresses
= new HashSet
<>(numbers
.size());
1451 final Set
<SignalServiceAddress
> missingUuids
= new HashSet
<>();
1453 for (String number
: numbers
) {
1454 final SignalServiceAddress resolvedAddress
= canonicalizeAndResolveSignalServiceAddress(number
);
1455 if (resolvedAddress
.getUuid().isPresent()) {
1456 signalServiceAddresses
.add(resolvedAddress
);
1458 missingUuids
.add(resolvedAddress
);
1462 Map
<String
, UUID
> registeredUsers
;
1464 registeredUsers
= accountManager
.getRegisteredUsers(getIasKeyStore(),
1465 missingUuids
.stream().map(a
-> a
.getNumber().get()).collect(Collectors
.toSet()),
1467 } catch (IOException
| Quote
.InvalidQuoteFormatException
| UnauthenticatedQuoteException
| SignatureException
| UnauthenticatedResponseException e
) {
1468 logger
.warn("Failed to resolve uuids from server, ignoring: {}", e
.getMessage());
1469 registeredUsers
= new HashMap
<>();
1472 for (SignalServiceAddress address
: missingUuids
) {
1473 final String number
= address
.getNumber().get();
1474 if (registeredUsers
.containsKey(number
)) {
1475 final SignalServiceAddress newAddress
= resolveSignalServiceAddress(new SignalServiceAddress(
1476 registeredUsers
.get(number
),
1478 signalServiceAddresses
.add(newAddress
);
1480 signalServiceAddresses
.add(address
);
1484 return signalServiceAddresses
;
1487 private Pair
<Long
, List
<SendMessageResult
>> sendMessage(
1488 SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
1489 ) throws IOException
{
1490 recipients
= recipients
.stream().map(this::resolveSignalServiceAddress
).collect(Collectors
.toSet());
1491 final long timestamp
= System
.currentTimeMillis();
1492 messageBuilder
.withTimestamp(timestamp
);
1493 getOrCreateMessagePipe();
1494 getOrCreateUnidentifiedMessagePipe();
1495 SignalServiceDataMessage message
= null;
1497 message
= messageBuilder
.build();
1498 if (message
.getGroupContext().isPresent()) {
1500 SignalServiceMessageSender messageSender
= createMessageSender();
1501 final boolean isRecipientUpdate
= false;
1502 List
<SendMessageResult
> result
= messageSender
.sendMessage(new ArrayList
<>(recipients
),
1503 unidentifiedAccessHelper
.getAccessFor(recipients
),
1506 for (SendMessageResult r
: result
) {
1507 if (r
.getIdentityFailure() != null) {
1508 account
.getSignalProtocolStore()
1509 .saveIdentity(r
.getAddress(),
1510 r
.getIdentityFailure().getIdentityKey(),
1511 TrustLevel
.UNTRUSTED
);
1514 return new Pair
<>(timestamp
, result
);
1515 } catch (UntrustedIdentityException e
) {
1516 account
.getSignalProtocolStore()
1517 .saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()),
1519 TrustLevel
.UNTRUSTED
);
1520 return new Pair
<>(timestamp
, List
.of());
1523 // Send to all individually, so sync messages are sent correctly
1524 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1525 for (SignalServiceAddress address
: recipients
) {
1526 ContactInfo contact
= account
.getContactStore().getContact(address
);
1527 if (contact
!= null) {
1528 messageBuilder
.withExpiration(contact
.messageExpirationTime
);
1529 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
1531 messageBuilder
.withExpiration(0);
1532 messageBuilder
.withProfileKey(null);
1534 message
= messageBuilder
.build();
1535 if (address
.matches(account
.getSelfAddress())) {
1536 results
.add(sendSelfMessage(message
));
1538 results
.add(sendMessage(address
, message
));
1541 return new Pair
<>(timestamp
, results
);
1544 if (message
!= null && message
.isEndSession()) {
1545 for (SignalServiceAddress recipient
: recipients
) {
1546 handleEndSession(recipient
);
1553 private SendMessageResult
sendSelfMessage(SignalServiceDataMessage message
) throws IOException
{
1554 SignalServiceMessageSender messageSender
= createMessageSender();
1556 SignalServiceAddress recipient
= account
.getSelfAddress();
1558 final Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= unidentifiedAccessHelper
.getAccessFor(recipient
);
1559 SentTranscriptMessage transcript
= new SentTranscriptMessage(Optional
.of(recipient
),
1560 message
.getTimestamp(),
1562 message
.getExpiresInSeconds(),
1563 Map
.of(recipient
, unidentifiedAccess
.isPresent()),
1565 SignalServiceSyncMessage syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
1568 long startTime
= System
.currentTimeMillis();
1569 messageSender
.sendMessage(syncMessage
, unidentifiedAccess
);
1570 return SendMessageResult
.success(recipient
,
1571 unidentifiedAccess
.isPresent(),
1573 System
.currentTimeMillis() - startTime
);
1574 } catch (UntrustedIdentityException e
) {
1575 account
.getSignalProtocolStore()
1576 .saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()),
1578 TrustLevel
.UNTRUSTED
);
1579 return SendMessageResult
.identityFailure(recipient
, e
.getIdentityKey());
1583 private SendMessageResult
sendMessage(
1584 SignalServiceAddress address
, SignalServiceDataMessage message
1585 ) throws IOException
{
1586 SignalServiceMessageSender messageSender
= createMessageSender();
1589 return messageSender
.sendMessage(address
, unidentifiedAccessHelper
.getAccessFor(address
), message
);
1590 } catch (UntrustedIdentityException e
) {
1591 account
.getSignalProtocolStore()
1592 .saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()),
1594 TrustLevel
.UNTRUSTED
);
1595 return SendMessageResult
.identityFailure(address
, e
.getIdentityKey());
1599 private SignalServiceContent
decryptMessage(SignalServiceEnvelope envelope
) throws InvalidMetadataMessageException
, ProtocolInvalidMessageException
, ProtocolDuplicateMessageException
, ProtocolLegacyMessageException
, ProtocolInvalidKeyIdException
, InvalidMetadataVersionException
, ProtocolInvalidVersionException
, ProtocolNoSessionException
, ProtocolInvalidKeyException
, SelfSendException
, UnsupportedDataMessageException
, org
.whispersystems
.libsignal
.UntrustedIdentityException
{
1600 SignalServiceCipher cipher
= new SignalServiceCipher(account
.getSelfAddress(),
1601 account
.getSignalProtocolStore(),
1602 certificateValidator
);
1604 return cipher
.decrypt(envelope
);
1605 } catch (ProtocolUntrustedIdentityException e
) {
1606 if (e
.getCause() instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
) {
1607 org
.whispersystems
.libsignal
.UntrustedIdentityException identityException
= (org
.whispersystems
.libsignal
.UntrustedIdentityException
) e
1609 account
.getSignalProtocolStore()
1610 .saveIdentity(resolveSignalServiceAddress(identityException
.getName()),
1611 identityException
.getUntrustedIdentity(),
1612 TrustLevel
.UNTRUSTED
);
1613 throw identityException
;
1615 throw new AssertionError(e
);
1619 private void handleEndSession(SignalServiceAddress source
) {
1620 account
.getSignalProtocolStore().deleteAllSessions(source
);
1623 private static int currentTimeDays() {
1624 return (int) TimeUnit
.MILLISECONDS
.toDays(System
.currentTimeMillis());
1627 private GroupsV2AuthorizationString
getGroupAuthForToday(
1628 final GroupSecretParams groupSecretParams
1629 ) throws IOException
{
1630 final int today
= currentTimeDays();
1631 // Returns credentials for the next 7 days
1632 final HashMap
<Integer
, AuthCredentialResponse
> credentials
= groupsV2Api
.getCredentials(today
);
1633 // TODO cache credentials until they expire
1634 AuthCredentialResponse authCredentialResponse
= credentials
.get(today
);
1636 return groupsV2Api
.getGroupsV2AuthorizationString(account
.getUuid(),
1639 authCredentialResponse
);
1640 } catch (VerificationFailedException e
) {
1641 throw new IOException(e
);
1645 private List
<HandleAction
> handleSignalServiceDataMessage(
1646 SignalServiceDataMessage message
,
1648 SignalServiceAddress source
,
1649 SignalServiceAddress destination
,
1650 boolean ignoreAttachments
1652 List
<HandleAction
> actions
= new ArrayList
<>();
1653 if (message
.getGroupContext().isPresent()) {
1654 if (message
.getGroupContext().get().getGroupV1().isPresent()) {
1655 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1656 GroupIdV1 groupId
= GroupId
.v1(groupInfo
.getGroupId());
1657 GroupInfo group
= account
.getGroupStore().getGroup(groupId
);
1658 if (group
== null || group
instanceof GroupInfoV1
) {
1659 GroupInfoV1 groupV1
= (GroupInfoV1
) group
;
1660 switch (groupInfo
.getType()) {
1662 if (groupV1
== null) {
1663 groupV1
= new GroupInfoV1(groupId
);
1666 if (groupInfo
.getAvatar().isPresent()) {
1667 SignalServiceAttachment avatar
= groupInfo
.getAvatar().get();
1668 if (avatar
.isPointer()) {
1670 retrieveGroupAvatarAttachment(avatar
.asPointer(), groupV1
.getGroupId());
1671 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1672 logger
.warn("Failed to retrieve avatar for group {}, ignoring: {}",
1679 if (groupInfo
.getName().isPresent()) {
1680 groupV1
.name
= groupInfo
.getName().get();
1683 if (groupInfo
.getMembers().isPresent()) {
1684 groupV1
.addMembers(groupInfo
.getMembers()
1687 .map(this::resolveSignalServiceAddress
)
1688 .collect(Collectors
.toSet()));
1691 account
.getGroupStore().updateGroup(groupV1
);
1695 if (groupV1
== null && !isSync
) {
1696 actions
.add(new SendGroupInfoRequestAction(source
, groupId
));
1700 if (groupV1
!= null) {
1701 groupV1
.removeMember(source
);
1702 account
.getGroupStore().updateGroup(groupV1
);
1707 if (groupV1
!= null && !isSync
) {
1708 actions
.add(new SendGroupUpdateAction(source
, groupV1
.getGroupId()));
1713 // Received a group v1 message for a v2 group
1716 if (message
.getGroupContext().get().getGroupV2().isPresent()) {
1717 final SignalServiceGroupV2 groupContext
= message
.getGroupContext().get().getGroupV2().get();
1718 final GroupMasterKey groupMasterKey
= groupContext
.getMasterKey();
1720 getOrMigrateGroup(groupMasterKey
,
1721 groupContext
.getRevision(),
1722 groupContext
.hasSignedGroupChange() ? groupContext
.getSignedGroupChange() : null);
1726 final SignalServiceAddress conversationPartnerAddress
= isSync ? destination
: source
;
1727 if (conversationPartnerAddress
!= null && message
.isEndSession()) {
1728 handleEndSession(conversationPartnerAddress
);
1730 if (message
.isExpirationUpdate() || message
.getBody().isPresent()) {
1731 if (message
.getGroupContext().isPresent()) {
1732 if (message
.getGroupContext().get().getGroupV1().isPresent()) {
1733 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1734 GroupInfoV1 group
= account
.getGroupStore().getOrCreateGroupV1(GroupId
.v1(groupInfo
.getGroupId()));
1735 if (group
!= null) {
1736 if (group
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1737 group
.messageExpirationTime
= message
.getExpiresInSeconds();
1738 account
.getGroupStore().updateGroup(group
);
1741 } else if (message
.getGroupContext().get().getGroupV2().isPresent()) {
1742 // disappearing message timer already stored in the DecryptedGroup
1744 } else if (conversationPartnerAddress
!= null) {
1745 ContactInfo contact
= account
.getContactStore().getContact(conversationPartnerAddress
);
1746 if (contact
== null) {
1747 contact
= new ContactInfo(conversationPartnerAddress
);
1749 if (contact
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1750 contact
.messageExpirationTime
= message
.getExpiresInSeconds();
1751 account
.getContactStore().updateContact(contact
);
1755 if (message
.getAttachments().isPresent() && !ignoreAttachments
) {
1756 for (SignalServiceAttachment attachment
: message
.getAttachments().get()) {
1757 if (attachment
.isPointer()) {
1759 retrieveAttachment(attachment
.asPointer());
1760 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1761 logger
.warn("Failed to retrieve attachment ({}), ignoring: {}",
1762 attachment
.asPointer().getRemoteId(),
1768 if (message
.getProfileKey().isPresent() && message
.getProfileKey().get().length
== 32) {
1769 final ProfileKey profileKey
;
1771 profileKey
= new ProfileKey(message
.getProfileKey().get());
1772 } catch (InvalidInputException e
) {
1773 throw new AssertionError(e
);
1775 if (source
.matches(account
.getSelfAddress())) {
1776 this.account
.setProfileKey(profileKey
);
1778 this.account
.getProfileStore().storeProfileKey(source
, profileKey
);
1780 if (message
.getPreviews().isPresent()) {
1781 final List
<SignalServiceDataMessage
.Preview
> previews
= message
.getPreviews().get();
1782 for (SignalServiceDataMessage
.Preview preview
: previews
) {
1783 if (preview
.getImage().isPresent() && preview
.getImage().get().isPointer()) {
1784 SignalServiceAttachmentPointer attachment
= preview
.getImage().get().asPointer();
1786 retrieveAttachment(attachment
);
1787 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1788 logger
.warn("Failed to retrieve preview image ({}), ignoring: {}",
1789 attachment
.getRemoteId(),
1795 if (message
.getQuote().isPresent()) {
1796 final SignalServiceDataMessage
.Quote quote
= message
.getQuote().get();
1798 for (SignalServiceDataMessage
.Quote
.QuotedAttachment quotedAttachment
: quote
.getAttachments()) {
1799 final SignalServiceAttachment attachment
= quotedAttachment
.getThumbnail();
1800 if (attachment
!= null && attachment
.isPointer()) {
1802 retrieveAttachment(attachment
.asPointer());
1803 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1804 logger
.warn("Failed to retrieve quote attachment thumbnail ({}), ignoring: {}",
1805 attachment
.asPointer().getRemoteId(),
1811 if (message
.getSticker().isPresent()) {
1812 final SignalServiceDataMessage
.Sticker messageSticker
= message
.getSticker().get();
1813 Sticker sticker
= account
.getStickerStore().getSticker(messageSticker
.getPackId());
1814 if (sticker
== null) {
1815 sticker
= new Sticker(messageSticker
.getPackId(), messageSticker
.getPackKey());
1816 account
.getStickerStore().updateSticker(sticker
);
1822 private GroupInfoV2
getOrMigrateGroup(
1823 final GroupMasterKey groupMasterKey
, final int revision
, final byte[] signedGroupChange
1825 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
1827 GroupIdV2 groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
1828 GroupInfo groupInfo
= account
.getGroupStore().getGroup(groupId
);
1829 final GroupInfoV2 groupInfoV2
;
1830 if (groupInfo
instanceof GroupInfoV1
) {
1831 // Received a v2 group message for a v1 group, we need to locally migrate the group
1832 account
.getGroupStore().deleteGroup(groupInfo
.getGroupId());
1833 groupInfoV2
= new GroupInfoV2(groupId
, groupMasterKey
);
1834 logger
.info("Locally migrated group {} to group v2, id: {}",
1835 groupInfo
.getGroupId().toBase64(),
1836 groupInfoV2
.getGroupId().toBase64());
1837 } else if (groupInfo
instanceof GroupInfoV2
) {
1838 groupInfoV2
= (GroupInfoV2
) groupInfo
;
1840 groupInfoV2
= new GroupInfoV2(groupId
, groupMasterKey
);
1843 if (groupInfoV2
.getGroup() == null || groupInfoV2
.getGroup().getRevision() < revision
) {
1844 DecryptedGroup group
= null;
1845 if (signedGroupChange
!= null
1846 && groupInfoV2
.getGroup() != null
1847 && groupInfoV2
.getGroup().getRevision() + 1 == revision
) {
1848 group
= groupHelper
.getUpdatedDecryptedGroup(groupInfoV2
.getGroup(), signedGroupChange
, groupMasterKey
);
1850 if (group
== null) {
1851 group
= groupHelper
.getDecryptedGroup(groupSecretParams
);
1853 if (group
!= null) {
1854 storeProfileKeysFromMembers(group
);
1855 final String avatar
= group
.getAvatar();
1856 if (avatar
!= null && !avatar
.isEmpty()) {
1858 retrieveGroupAvatar(groupId
, groupSecretParams
, avatar
);
1859 } catch (IOException e
) {
1860 logger
.warn("Failed to download group avatar, ignoring: {}", e
.getMessage());
1864 groupInfoV2
.setGroup(group
);
1865 account
.getGroupStore().updateGroup(groupInfoV2
);
1871 private void storeProfileKeysFromMembers(final DecryptedGroup group
) {
1872 for (DecryptedMember member
: group
.getMembersList()) {
1873 final SignalServiceAddress address
= resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil
.parseOrThrow(
1874 member
.getUuid().toByteArray()), null));
1876 account
.getProfileStore()
1877 .storeProfileKey(address
, new ProfileKey(member
.getProfileKey().toByteArray()));
1878 } catch (InvalidInputException ignored
) {
1883 private void retryFailedReceivedMessages(
1884 ReceiveMessageHandler handler
, boolean ignoreAttachments
1886 final File cachePath
= getMessageCachePath();
1887 if (!cachePath
.exists()) {
1890 for (final File dir
: Objects
.requireNonNull(cachePath
.listFiles())) {
1891 if (!dir
.isDirectory()) {
1892 retryFailedReceivedMessage(handler
, ignoreAttachments
, dir
);
1896 for (final File fileEntry
: Objects
.requireNonNull(dir
.listFiles())) {
1897 if (!fileEntry
.isFile()) {
1900 retryFailedReceivedMessage(handler
, ignoreAttachments
, fileEntry
);
1902 // Try to delete directory if empty
1907 private void retryFailedReceivedMessage(
1908 final ReceiveMessageHandler handler
, final boolean ignoreAttachments
, final File fileEntry
1910 SignalServiceEnvelope envelope
;
1912 envelope
= MessageCacheUtils
.loadEnvelope(fileEntry
);
1913 if (envelope
== null) {
1916 } catch (IOException e
) {
1917 e
.printStackTrace();
1920 SignalServiceContent content
= null;
1921 if (!envelope
.isReceipt()) {
1923 content
= decryptMessage(envelope
);
1924 } catch (org
.whispersystems
.libsignal
.UntrustedIdentityException e
) {
1926 } catch (Exception er
) {
1927 // All other errors are not recoverable, so delete the cached message
1929 Files
.delete(fileEntry
.toPath());
1930 } catch (IOException e
) {
1931 logger
.warn("Failed to delete cached message file “{}”, ignoring: {}", fileEntry
, e
.getMessage());
1935 List
<HandleAction
> actions
= handleMessage(envelope
, content
, ignoreAttachments
);
1936 for (HandleAction action
: actions
) {
1938 action
.execute(this);
1939 } catch (Throwable e
) {
1940 e
.printStackTrace();
1945 handler
.handleMessage(envelope
, content
, null);
1947 Files
.delete(fileEntry
.toPath());
1948 } catch (IOException e
) {
1949 logger
.warn("Failed to delete cached message file “{}”, ignoring: {}", fileEntry
, e
.getMessage());
1953 public void receiveMessages(
1956 boolean returnOnTimeout
,
1957 boolean ignoreAttachments
,
1958 ReceiveMessageHandler handler
1959 ) throws IOException
{
1960 retryFailedReceivedMessages(handler
, ignoreAttachments
);
1962 Set
<HandleAction
> queuedActions
= null;
1964 getOrCreateMessagePipe();
1966 boolean hasCaughtUpWithOldMessages
= false;
1969 SignalServiceEnvelope envelope
;
1970 SignalServiceContent content
= null;
1971 Exception exception
= null;
1972 final long now
= new Date().getTime();
1974 Optional
<SignalServiceEnvelope
> result
= messagePipe
.readOrEmpty(timeout
, unit
, envelope1
-> {
1975 // store message on disk, before acknowledging receipt to the server
1977 String source
= envelope1
.getSourceE164().isPresent() ? envelope1
.getSourceE164().get() : "";
1978 File cacheFile
= getMessageCacheFile(source
, now
, envelope1
.getTimestamp());
1979 MessageCacheUtils
.storeEnvelope(envelope1
, cacheFile
);
1980 } catch (IOException e
) {
1981 logger
.warn("Failed to store encrypted message in disk cache, ignoring: {}", e
.getMessage());
1984 if (result
.isPresent()) {
1985 envelope
= result
.get();
1987 // Received indicator that server queue is empty
1988 hasCaughtUpWithOldMessages
= true;
1990 if (queuedActions
!= null) {
1991 for (HandleAction action
: queuedActions
) {
1993 action
.execute(this);
1994 } catch (Throwable e
) {
1995 e
.printStackTrace();
1999 queuedActions
.clear();
2000 queuedActions
= null;
2003 // Continue to wait another timeout for new messages
2006 } catch (TimeoutException e
) {
2007 if (returnOnTimeout
) return;
2009 } catch (InvalidVersionException e
) {
2010 logger
.warn("Error while receiving messages, ignoring: {}", e
.getMessage());
2014 if (envelope
.hasSource()) {
2015 // Store uuid if we don't have it already
2016 SignalServiceAddress source
= envelope
.getSourceAddress();
2017 resolveSignalServiceAddress(source
);
2019 if (!envelope
.isReceipt()) {
2021 content
= decryptMessage(envelope
);
2022 } catch (Exception e
) {
2025 List
<HandleAction
> actions
= handleMessage(envelope
, content
, ignoreAttachments
);
2026 if (hasCaughtUpWithOldMessages
) {
2027 for (HandleAction action
: actions
) {
2029 action
.execute(this);
2030 } catch (Throwable e
) {
2031 e
.printStackTrace();
2035 if (queuedActions
== null) {
2036 queuedActions
= new HashSet
<>();
2038 queuedActions
.addAll(actions
);
2042 if (!isMessageBlocked(envelope
, content
)) {
2043 handler
.handleMessage(envelope
, content
, exception
);
2045 if (!(exception
instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
)) {
2046 File cacheFile
= null;
2048 String source
= envelope
.getSourceE164().isPresent() ? envelope
.getSourceE164().get() : "";
2049 cacheFile
= getMessageCacheFile(source
, now
, envelope
.getTimestamp());
2050 Files
.delete(cacheFile
.toPath());
2051 // Try to delete directory if empty
2052 getMessageCachePath().delete();
2053 } catch (IOException e
) {
2054 logger
.warn("Failed to delete cached message file “{}”, ignoring: {}", cacheFile
, e
.getMessage());
2060 private boolean isMessageBlocked(
2061 SignalServiceEnvelope envelope
, SignalServiceContent content
2063 SignalServiceAddress source
;
2064 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
2065 source
= envelope
.getSourceAddress();
2066 } else if (content
!= null) {
2067 source
= content
.getSender();
2071 ContactInfo sourceContact
= account
.getContactStore().getContact(source
);
2072 if (sourceContact
!= null && sourceContact
.blocked
) {
2076 if (content
!= null && content
.getDataMessage().isPresent()) {
2077 SignalServiceDataMessage message
= content
.getDataMessage().get();
2078 if (message
.getGroupContext().isPresent()) {
2079 if (message
.getGroupContext().get().getGroupV1().isPresent()) {
2080 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
2081 if (groupInfo
.getType() != SignalServiceGroup
.Type
.DELIVER
) {
2085 GroupId groupId
= GroupUtils
.getGroupId(message
.getGroupContext().get());
2086 GroupInfo group
= account
.getGroupStore().getGroup(groupId
);
2087 if (group
!= null && group
.isBlocked()) {
2095 private List
<HandleAction
> handleMessage(
2096 SignalServiceEnvelope envelope
, SignalServiceContent content
, boolean ignoreAttachments
2098 List
<HandleAction
> actions
= new ArrayList
<>();
2099 if (content
!= null) {
2100 final SignalServiceAddress sender
;
2101 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
2102 sender
= envelope
.getSourceAddress();
2104 sender
= content
.getSender();
2106 // Store uuid if we don't have it already
2107 resolveSignalServiceAddress(sender
);
2109 if (content
.getDataMessage().isPresent()) {
2110 SignalServiceDataMessage message
= content
.getDataMessage().get();
2112 if (content
.isNeedsReceipt()) {
2113 actions
.add(new SendReceiptAction(sender
, message
.getTimestamp()));
2116 actions
.addAll(handleSignalServiceDataMessage(message
,
2119 account
.getSelfAddress(),
2120 ignoreAttachments
));
2122 if (content
.getSyncMessage().isPresent()) {
2123 account
.setMultiDevice(true);
2124 SignalServiceSyncMessage syncMessage
= content
.getSyncMessage().get();
2125 if (syncMessage
.getSent().isPresent()) {
2126 SentTranscriptMessage message
= syncMessage
.getSent().get();
2127 final SignalServiceAddress destination
= message
.getDestination().orNull();
2128 actions
.addAll(handleSignalServiceDataMessage(message
.getMessage(),
2132 ignoreAttachments
));
2134 if (syncMessage
.getRequest().isPresent()) {
2135 RequestMessage rm
= syncMessage
.getRequest().get();
2136 if (rm
.isContactsRequest()) {
2137 actions
.add(SendSyncContactsAction
.create());
2139 if (rm
.isGroupsRequest()) {
2140 actions
.add(SendSyncGroupsAction
.create());
2142 if (rm
.isBlockedListRequest()) {
2143 actions
.add(SendSyncBlockedListAction
.create());
2145 // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest();
2147 if (syncMessage
.getGroups().isPresent()) {
2148 File tmpFile
= null;
2150 tmpFile
= IOUtils
.createTempFile();
2151 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(syncMessage
.getGroups()
2153 .asPointer(), tmpFile
)) {
2154 DeviceGroupsInputStream s
= new DeviceGroupsInputStream(attachmentAsStream
);
2156 while ((g
= s
.read()) != null) {
2157 GroupInfoV1 syncGroup
= account
.getGroupStore()
2158 .getOrCreateGroupV1(GroupId
.v1(g
.getId()));
2159 if (syncGroup
!= null) {
2160 if (g
.getName().isPresent()) {
2161 syncGroup
.name
= g
.getName().get();
2163 syncGroup
.addMembers(g
.getMembers()
2165 .map(this::resolveSignalServiceAddress
)
2166 .collect(Collectors
.toSet()));
2167 if (!g
.isActive()) {
2168 syncGroup
.removeMember(account
.getSelfAddress());
2170 // Add ourself to the member set as it's marked as active
2171 syncGroup
.addMembers(List
.of(account
.getSelfAddress()));
2173 syncGroup
.blocked
= g
.isBlocked();
2174 if (g
.getColor().isPresent()) {
2175 syncGroup
.color
= g
.getColor().get();
2178 if (g
.getAvatar().isPresent()) {
2179 retrieveGroupAvatarAttachment(g
.getAvatar().get(), syncGroup
.getGroupId());
2181 syncGroup
.inboxPosition
= g
.getInboxPosition().orNull();
2182 syncGroup
.archived
= g
.isArchived();
2183 account
.getGroupStore().updateGroup(syncGroup
);
2187 } catch (Exception e
) {
2188 logger
.warn("Failed to handle received sync groups “{}”, ignoring: {}",
2191 e
.printStackTrace();
2193 if (tmpFile
!= null) {
2195 Files
.delete(tmpFile
.toPath());
2196 } catch (IOException e
) {
2197 logger
.warn("Failed to delete received groups temp file “{}”, ignoring: {}",
2204 if (syncMessage
.getBlockedList().isPresent()) {
2205 final BlockedListMessage blockedListMessage
= syncMessage
.getBlockedList().get();
2206 for (SignalServiceAddress address
: blockedListMessage
.getAddresses()) {
2207 setContactBlocked(resolveSignalServiceAddress(address
), true);
2209 for (GroupId groupId
: blockedListMessage
.getGroupIds()
2211 .map(GroupId
::unknownVersion
)
2212 .collect(Collectors
.toSet())) {
2214 setGroupBlocked(groupId
, true);
2215 } catch (GroupNotFoundException e
) {
2216 logger
.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}",
2217 groupId
.toBase64());
2221 if (syncMessage
.getContacts().isPresent()) {
2222 File tmpFile
= null;
2224 tmpFile
= IOUtils
.createTempFile();
2225 final ContactsMessage contactsMessage
= syncMessage
.getContacts().get();
2226 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(contactsMessage
.getContactsStream()
2227 .asPointer(), tmpFile
)) {
2228 DeviceContactsInputStream s
= new DeviceContactsInputStream(attachmentAsStream
);
2229 if (contactsMessage
.isComplete()) {
2230 account
.getContactStore().clear();
2233 while ((c
= s
.read()) != null) {
2234 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
2235 account
.setProfileKey(c
.getProfileKey().get());
2237 final SignalServiceAddress address
= resolveSignalServiceAddress(c
.getAddress());
2238 ContactInfo contact
= account
.getContactStore().getContact(address
);
2239 if (contact
== null) {
2240 contact
= new ContactInfo(address
);
2242 if (c
.getName().isPresent()) {
2243 contact
.name
= c
.getName().get();
2245 if (c
.getColor().isPresent()) {
2246 contact
.color
= c
.getColor().get();
2248 if (c
.getProfileKey().isPresent()) {
2249 account
.getProfileStore().storeProfileKey(address
, c
.getProfileKey().get());
2251 if (c
.getVerified().isPresent()) {
2252 final VerifiedMessage verifiedMessage
= c
.getVerified().get();
2253 account
.getSignalProtocolStore()
2254 .setIdentityTrustLevel(verifiedMessage
.getDestination(),
2255 verifiedMessage
.getIdentityKey(),
2256 TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
2258 if (c
.getExpirationTimer().isPresent()) {
2259 contact
.messageExpirationTime
= c
.getExpirationTimer().get();
2261 contact
.blocked
= c
.isBlocked();
2262 contact
.inboxPosition
= c
.getInboxPosition().orNull();
2263 contact
.archived
= c
.isArchived();
2264 account
.getContactStore().updateContact(contact
);
2266 if (c
.getAvatar().isPresent()) {
2267 retrieveContactAvatarAttachment(c
.getAvatar().get(), contact
.number
);
2271 } catch (Exception e
) {
2272 e
.printStackTrace();
2274 if (tmpFile
!= null) {
2276 Files
.delete(tmpFile
.toPath());
2277 } catch (IOException e
) {
2278 logger
.warn("Failed to delete received contacts temp file “{}”, ignoring: {}",
2285 if (syncMessage
.getVerified().isPresent()) {
2286 final VerifiedMessage verifiedMessage
= syncMessage
.getVerified().get();
2287 account
.getSignalProtocolStore()
2288 .setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage
.getDestination()),
2289 verifiedMessage
.getIdentityKey(),
2290 TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
2292 if (syncMessage
.getStickerPackOperations().isPresent()) {
2293 final List
<StickerPackOperationMessage
> stickerPackOperationMessages
= syncMessage
.getStickerPackOperations()
2295 for (StickerPackOperationMessage m
: stickerPackOperationMessages
) {
2296 if (!m
.getPackId().isPresent()) {
2299 Sticker sticker
= account
.getStickerStore().getSticker(m
.getPackId().get());
2300 if (sticker
== null) {
2301 if (!m
.getPackKey().isPresent()) {
2304 sticker
= new Sticker(m
.getPackId().get(), m
.getPackKey().get());
2306 sticker
.setInstalled(!m
.getType().isPresent()
2307 || m
.getType().get() == StickerPackOperationMessage
.Type
.INSTALL
);
2308 account
.getStickerStore().updateSticker(sticker
);
2311 if (syncMessage
.getConfiguration().isPresent()) {
2319 private File
getContactAvatarFile(String number
) {
2320 return new File(pathConfig
.getAvatarsPath(), "contact-" + number
);
2323 private File
retrieveContactAvatarAttachment(
2324 SignalServiceAttachment attachment
, String number
2325 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2326 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
2327 if (attachment
.isPointer()) {
2328 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
2329 return retrieveAttachment(pointer
, getContactAvatarFile(number
), false);
2331 SignalServiceAttachmentStream stream
= attachment
.asStream();
2332 return AttachmentUtils
.retrieveAttachment(stream
, getContactAvatarFile(number
));
2336 private File
getGroupAvatarFile(GroupId groupId
) {
2337 return new File(pathConfig
.getAvatarsPath(), "group-" + groupId
.toBase64().replace("/", "_"));
2340 private File
retrieveGroupAvatarAttachment(
2341 SignalServiceAttachment attachment
, GroupId groupId
2342 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2343 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
2344 if (attachment
.isPointer()) {
2345 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
2346 return retrieveAttachment(pointer
, getGroupAvatarFile(groupId
), false);
2348 SignalServiceAttachmentStream stream
= attachment
.asStream();
2349 return AttachmentUtils
.retrieveAttachment(stream
, getGroupAvatarFile(groupId
));
2353 private File
retrieveGroupAvatar(
2354 GroupId groupId
, GroupSecretParams groupSecretParams
, String cdnKey
2355 ) throws IOException
{
2356 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
2357 SignalServiceMessageReceiver receiver
= getOrCreateMessageReceiver();
2358 File outputFile
= getGroupAvatarFile(groupId
);
2359 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
2361 File tmpFile
= IOUtils
.createTempFile();
2362 tmpFile
.deleteOnExit();
2363 try (InputStream input
= receiver
.retrieveGroupsV2ProfileAvatar(cdnKey
,
2365 ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
)) {
2366 byte[] encryptedData
= IOUtils
.readFully(input
);
2368 byte[] decryptedData
= groupOperations
.decryptAvatar(encryptedData
);
2369 try (OutputStream output
= new FileOutputStream(outputFile
)) {
2370 output
.write(decryptedData
);
2374 Files
.delete(tmpFile
.toPath());
2375 } catch (IOException e
) {
2376 logger
.warn("Failed to delete received group avatar temp file “{}”, ignoring: {}",
2384 private File
getProfileAvatarFile(SignalServiceAddress address
) {
2385 return new File(pathConfig
.getAvatarsPath(), "profile-" + address
.getLegacyIdentifier());
2388 private File
retrieveProfileAvatar(
2389 SignalServiceAddress address
, String avatarPath
, ProfileKey profileKey
2390 ) throws IOException
{
2391 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
2392 SignalServiceMessageReceiver receiver
= getOrCreateMessageReceiver();
2393 File outputFile
= getProfileAvatarFile(address
);
2395 File tmpFile
= IOUtils
.createTempFile();
2396 try (InputStream input
= receiver
.retrieveProfileAvatar(avatarPath
,
2399 ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
)) {
2400 // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
2401 IOUtils
.copyStreamToFile(input
, outputFile
, (int) ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
);
2404 Files
.delete(tmpFile
.toPath());
2405 } catch (IOException e
) {
2406 logger
.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}",
2414 public File
getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId
) {
2415 return new File(pathConfig
.getAttachmentsPath(), attachmentId
.toString());
2418 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2419 IOUtils
.createPrivateDirectories(pathConfig
.getAttachmentsPath());
2420 return retrieveAttachment(pointer
, getAttachmentFile(pointer
.getRemoteId()), true);
2423 private File
retrieveAttachment(
2424 SignalServiceAttachmentPointer pointer
, File outputFile
, boolean storePreview
2425 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2426 if (storePreview
&& pointer
.getPreview().isPresent()) {
2427 File previewFile
= new File(outputFile
+ ".preview");
2428 try (OutputStream output
= new FileOutputStream(previewFile
)) {
2429 byte[] preview
= pointer
.getPreview().get();
2430 output
.write(preview
, 0, preview
.length
);
2431 } catch (FileNotFoundException e
) {
2432 e
.printStackTrace();
2437 final SignalServiceMessageReceiver messageReceiver
= getOrCreateMessageReceiver();
2439 File tmpFile
= IOUtils
.createTempFile();
2440 try (InputStream input
= messageReceiver
.retrieveAttachment(pointer
,
2442 ServiceConfig
.MAX_ATTACHMENT_SIZE
)) {
2443 IOUtils
.copyStreamToFile(input
, outputFile
);
2446 Files
.delete(tmpFile
.toPath());
2447 } catch (IOException e
) {
2448 logger
.warn("Failed to delete received attachment temp file “{}”, ignoring: {}",
2456 private InputStream
retrieveAttachmentAsStream(
2457 SignalServiceAttachmentPointer pointer
, File tmpFile
2458 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2459 final SignalServiceMessageReceiver messageReceiver
= getOrCreateMessageReceiver();
2460 return messageReceiver
.retrieveAttachment(pointer
, tmpFile
, ServiceConfig
.MAX_ATTACHMENT_SIZE
);
2463 void sendGroups() throws IOException
, UntrustedIdentityException
{
2464 File groupsFile
= IOUtils
.createTempFile();
2467 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
2468 DeviceGroupsOutputStream out
= new DeviceGroupsOutputStream(fos
);
2469 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
2470 if (record instanceof GroupInfoV1
) {
2471 GroupInfoV1 groupInfo
= (GroupInfoV1
) record;
2472 out
.write(new DeviceGroup(groupInfo
.getGroupId().serialize(),
2473 Optional
.fromNullable(groupInfo
.name
),
2474 new ArrayList
<>(groupInfo
.getMembers()),
2475 createGroupAvatarAttachment(groupInfo
.getGroupId()),
2476 groupInfo
.isMember(account
.getSelfAddress()),
2477 Optional
.of(groupInfo
.messageExpirationTime
),
2478 Optional
.fromNullable(groupInfo
.color
),
2480 Optional
.fromNullable(groupInfo
.inboxPosition
),
2481 groupInfo
.archived
));
2486 if (groupsFile
.exists() && groupsFile
.length() > 0) {
2487 try (FileInputStream groupsFileStream
= new FileInputStream(groupsFile
)) {
2488 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
2489 .withStream(groupsFileStream
)
2490 .withContentType("application/octet-stream")
2491 .withLength(groupsFile
.length())
2494 sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
2499 Files
.delete(groupsFile
.toPath());
2500 } catch (IOException e
) {
2501 logger
.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile
, e
.getMessage());
2506 public void sendContacts() throws IOException
, UntrustedIdentityException
{
2507 File contactsFile
= IOUtils
.createTempFile();
2510 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
2511 DeviceContactsOutputStream out
= new DeviceContactsOutputStream(fos
);
2512 for (ContactInfo
record : account
.getContactStore().getContacts()) {
2513 VerifiedMessage verifiedMessage
= null;
2514 IdentityInfo currentIdentity
= account
.getSignalProtocolStore().getIdentity(record.getAddress());
2515 if (currentIdentity
!= null) {
2516 verifiedMessage
= new VerifiedMessage(record.getAddress(),
2517 currentIdentity
.getIdentityKey(),
2518 currentIdentity
.getTrustLevel().toVerifiedState(),
2519 currentIdentity
.getDateAdded().getTime());
2522 ProfileKey profileKey
= account
.getProfileStore().getProfileKey(record.getAddress());
2523 out
.write(new DeviceContact(record.getAddress(),
2524 Optional
.fromNullable(record.name
),
2525 createContactAvatarAttachment(record.number
),
2526 Optional
.fromNullable(record.color
),
2527 Optional
.fromNullable(verifiedMessage
),
2528 Optional
.fromNullable(profileKey
),
2530 Optional
.of(record.messageExpirationTime
),
2531 Optional
.fromNullable(record.inboxPosition
),
2535 if (account
.getProfileKey() != null) {
2536 // Send our own profile key as well
2537 out
.write(new DeviceContact(account
.getSelfAddress(),
2542 Optional
.of(account
.getProfileKey()),
2550 if (contactsFile
.exists() && contactsFile
.length() > 0) {
2551 try (FileInputStream contactsFileStream
= new FileInputStream(contactsFile
)) {
2552 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
2553 .withStream(contactsFileStream
)
2554 .withContentType("application/octet-stream")
2555 .withLength(contactsFile
.length())
2558 sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
, true)));
2563 Files
.delete(contactsFile
.toPath());
2564 } catch (IOException e
) {
2565 logger
.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile
, e
.getMessage());
2570 void sendBlockedList() throws IOException
, UntrustedIdentityException
{
2571 List
<SignalServiceAddress
> addresses
= new ArrayList
<>();
2572 for (ContactInfo
record : account
.getContactStore().getContacts()) {
2573 if (record.blocked
) {
2574 addresses
.add(record.getAddress());
2577 List
<byte[]> groupIds
= new ArrayList
<>();
2578 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
2579 if (record.isBlocked()) {
2580 groupIds
.add(record.getGroupId().serialize());
2583 sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
2586 private void sendVerifiedMessage(
2587 SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
2588 ) throws IOException
, UntrustedIdentityException
{
2589 VerifiedMessage verifiedMessage
= new VerifiedMessage(destination
,
2591 trustLevel
.toVerifiedState(),
2592 System
.currentTimeMillis());
2593 sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
2596 public List
<ContactInfo
> getContacts() {
2597 return account
.getContactStore().getContacts();
2600 public ContactInfo
getContact(String number
) {
2601 return account
.getContactStore().getContact(Utils
.getSignalServiceAddressFromIdentifier(number
));
2604 public GroupInfo
getGroup(GroupId groupId
) {
2605 return account
.getGroupStore().getGroup(groupId
);
2608 public List
<IdentityInfo
> getIdentities() {
2609 return account
.getSignalProtocolStore().getIdentities();
2612 public List
<IdentityInfo
> getIdentities(String number
) throws InvalidNumberException
{
2613 return account
.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number
));
2617 * Trust this the identity with this fingerprint
2619 * @param name username of the identity
2620 * @param fingerprint Fingerprint
2622 public boolean trustIdentityVerified(String name
, byte[] fingerprint
) throws InvalidNumberException
{
2623 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
2624 List
<IdentityInfo
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2628 for (IdentityInfo id
: ids
) {
2629 if (!Arrays
.equals(id
.getIdentityKey().serialize(), fingerprint
)) {
2633 account
.getSignalProtocolStore()
2634 .setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2636 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2637 } catch (IOException
| UntrustedIdentityException e
) {
2638 e
.printStackTrace();
2647 * Trust this the identity with this safety number
2649 * @param name username of the identity
2650 * @param safetyNumber Safety number
2652 public boolean trustIdentityVerifiedSafetyNumber(String name
, String safetyNumber
) throws InvalidNumberException
{
2653 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
2654 List
<IdentityInfo
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2658 for (IdentityInfo id
: ids
) {
2659 if (!safetyNumber
.equals(computeSafetyNumber(address
, id
.getIdentityKey()))) {
2663 account
.getSignalProtocolStore()
2664 .setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2666 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2667 } catch (IOException
| UntrustedIdentityException e
) {
2668 e
.printStackTrace();
2677 * Trust all keys of this identity without verification
2679 * @param name username of the identity
2681 public boolean trustIdentityAllKeys(String name
) {
2682 SignalServiceAddress address
= resolveSignalServiceAddress(name
);
2683 List
<IdentityInfo
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2687 for (IdentityInfo id
: ids
) {
2688 if (id
.getTrustLevel() == TrustLevel
.UNTRUSTED
) {
2689 account
.getSignalProtocolStore()
2690 .setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2692 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2693 } catch (IOException
| UntrustedIdentityException e
) {
2694 e
.printStackTrace();
2702 public String
computeSafetyNumber(
2703 SignalServiceAddress theirAddress
, IdentityKey theirIdentityKey
2705 return Utils
.computeSafetyNumber(ServiceConfig
.capabilities
.isUuid(),
2706 account
.getSelfAddress(),
2707 getIdentityKeyPair().getPublicKey(),
2712 void saveAccount() {
2716 public SignalServiceAddress
canonicalizeAndResolveSignalServiceAddress(String identifier
) throws InvalidNumberException
{
2717 String canonicalizedNumber
= UuidUtil
.isUuid(identifier
)
2719 : PhoneNumberFormatter
.formatNumber(identifier
, account
.getUsername());
2720 return resolveSignalServiceAddress(canonicalizedNumber
);
2723 public SignalServiceAddress
resolveSignalServiceAddress(String identifier
) {
2724 SignalServiceAddress address
= Utils
.getSignalServiceAddressFromIdentifier(identifier
);
2726 return resolveSignalServiceAddress(address
);
2729 public SignalServiceAddress
resolveSignalServiceAddress(SignalServiceAddress address
) {
2730 if (address
.matches(account
.getSelfAddress())) {
2731 return account
.getSelfAddress();
2734 return account
.getRecipientStore().resolveServiceAddress(address
);
2738 public void close() throws IOException
{
2739 if (messagePipe
!= null) {
2740 messagePipe
.shutdown();
2744 if (unidentifiedMessagePipe
!= null) {
2745 unidentifiedMessagePipe
.shutdown();
2746 unidentifiedMessagePipe
= null;
2752 public interface ReceiveMessageHandler
{
2754 void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent decryptedContent
, Throwable e
);