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 Set
<String
> registeredUsers
= contactDetails
.stream()
398 .map(ContactTokenDetails
::getNumber
)
399 .collect(Collectors
.toSet());
401 return numbers
.stream().collect(Collectors
.toMap(x
-> x
, registeredUsers
::contains
));
404 public void register(boolean voiceVerification
, String captcha
) throws IOException
{
405 account
.setPassword(KeyUtils
.createPassword());
407 // Resetting UUID, because registering doesn't work otherwise
408 account
.setUuid(null);
409 createSignalServiceAccountManager();
411 if (voiceVerification
) {
412 accountManager
.requestVoiceVerificationCode(Locale
.getDefault(),
413 Optional
.fromNullable(captcha
),
416 accountManager
.requestSmsVerificationCode(false, Optional
.fromNullable(captcha
), Optional
.absent());
419 account
.setRegistered(false);
423 public void updateAccountAttributes() throws IOException
{
424 accountManager
.setAccountAttributes(account
.getSignalingKey(),
425 account
.getSignalProtocolStore().getLocalRegistrationId(),
427 // set legacy pin only if no KBS master key is set
428 account
.getPinMasterKey() == null ? account
.getRegistrationLockPin() : null,
429 account
.getPinMasterKey() == null ?
null : account
.getPinMasterKey().deriveRegistrationLock(),
430 unidentifiedAccessHelper
.getSelfUnidentifiedAccessKey(),
431 unrestrictedUnidentifiedAccess
,
433 discoverableByPhoneNumber
);
436 public void setProfile(String name
, File avatar
) throws IOException
{
437 try (final StreamDetails streamDetails
= avatar
== null ?
null : Utils
.createStreamDetailsFromFile(avatar
)) {
438 accountManager
.setVersionedProfile(account
.getUuid(), account
.getProfileKey(), name
, streamDetails
);
442 public void unregister() throws IOException
{
443 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
444 // If this is the master device, other users can't send messages to this number anymore.
445 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
446 accountManager
.setGcmId(Optional
.absent());
448 account
.setRegistered(false);
452 public List
<DeviceInfo
> getLinkedDevices() throws IOException
{
453 List
<DeviceInfo
> devices
= accountManager
.getDevices();
454 account
.setMultiDevice(devices
.size() > 1);
459 public void removeLinkedDevices(int deviceId
) throws IOException
{
460 accountManager
.removeDevice(deviceId
);
461 List
<DeviceInfo
> devices
= accountManager
.getDevices();
462 account
.setMultiDevice(devices
.size() > 1);
466 public void addDeviceLink(URI linkUri
) throws IOException
, InvalidKeyException
{
467 DeviceLinkInfo info
= DeviceLinkInfo
.parseDeviceLinkUri(linkUri
);
469 addDevice(info
.deviceIdentifier
, info
.deviceKey
);
472 private void addDevice(String deviceIdentifier
, ECPublicKey deviceKey
) throws IOException
, InvalidKeyException
{
473 IdentityKeyPair identityKeyPair
= getIdentityKeyPair();
474 String verificationCode
= accountManager
.getNewDeviceVerificationCode();
476 accountManager
.addDevice(deviceIdentifier
,
479 Optional
.of(account
.getProfileKey().serialize()),
481 account
.setMultiDevice(true);
485 private List
<PreKeyRecord
> generatePreKeys() {
486 List
<PreKeyRecord
> records
= new ArrayList
<>(ServiceConfig
.PREKEY_BATCH_SIZE
);
488 final int offset
= account
.getPreKeyIdOffset();
489 for (int i
= 0; i
< ServiceConfig
.PREKEY_BATCH_SIZE
; i
++) {
490 int preKeyId
= (offset
+ i
) % Medium
.MAX_VALUE
;
491 ECKeyPair keyPair
= Curve
.generateKeyPair();
492 PreKeyRecord
record = new PreKeyRecord(preKeyId
, keyPair
);
497 account
.addPreKeys(records
);
503 private SignedPreKeyRecord
generateSignedPreKey(IdentityKeyPair identityKeyPair
) {
505 ECKeyPair keyPair
= Curve
.generateKeyPair();
506 byte[] signature
= Curve
.calculateSignature(identityKeyPair
.getPrivateKey(),
507 keyPair
.getPublicKey().serialize());
508 SignedPreKeyRecord
record = new SignedPreKeyRecord(account
.getNextSignedPreKeyId(),
509 System
.currentTimeMillis(),
513 account
.addSignedPreKey(record);
517 } catch (InvalidKeyException e
) {
518 throw new AssertionError(e
);
522 public void verifyAccount(
523 String verificationCode
, String pin
524 ) throws IOException
, KeyBackupSystemNoDataException
, KeyBackupServicePinException
{
525 verificationCode
= verificationCode
.replace("-", "");
526 account
.setSignalingKey(KeyUtils
.createSignalingKey());
527 VerifyAccountResponse response
;
529 response
= verifyAccountWithCode(verificationCode
, pin
, null);
530 } catch (LockedException e
) {
535 KbsPinData registrationLockData
= pinHelper
.getRegistrationLockData(pin
, e
);
536 if (registrationLockData
== null) {
540 String registrationLock
= registrationLockData
.getMasterKey().deriveRegistrationLock();
542 response
= verifyAccountWithCode(verificationCode
, null, registrationLock
);
543 } catch (LockedException _e
) {
544 throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
546 account
.setPinMasterKey(registrationLockData
.getMasterKey());
549 // TODO response.isStorageCapable()
550 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
552 account
.setRegistered(true);
553 account
.setUuid(UuidUtil
.parseOrNull(response
.getUuid()));
554 account
.setRegistrationLockPin(pin
);
555 account
.getSignalProtocolStore()
556 .saveIdentity(account
.getSelfAddress(),
557 getIdentityKeyPair().getPublicKey(),
558 TrustLevel
.TRUSTED_VERIFIED
);
564 private VerifyAccountResponse
verifyAccountWithCode(
565 final String verificationCode
, final String legacyPin
, final String registrationLock
566 ) throws IOException
{
567 return accountManager
.verifyAccountWithCode(verificationCode
,
568 account
.getSignalingKey(),
569 account
.getSignalProtocolStore().getLocalRegistrationId(),
573 unidentifiedAccessHelper
.getSelfUnidentifiedAccessKey(),
574 unrestrictedUnidentifiedAccess
,
576 discoverableByPhoneNumber
);
579 public void setRegistrationLockPin(Optional
<String
> pin
) throws IOException
, UnauthenticatedResponseException
{
580 if (pin
.isPresent()) {
581 final MasterKey masterKey
= account
.getPinMasterKey() != null
582 ? account
.getPinMasterKey()
583 : KeyUtils
.createMasterKey();
585 pinHelper
.setRegistrationLockPin(pin
.get(), masterKey
);
587 account
.setRegistrationLockPin(pin
.get());
588 account
.setPinMasterKey(masterKey
);
590 // Remove legacy registration lock
591 accountManager
.removeRegistrationLockV1();
594 pinHelper
.removeRegistrationLockPin();
596 account
.setRegistrationLockPin(null);
597 account
.setPinMasterKey(null);
602 void refreshPreKeys() throws IOException
{
603 List
<PreKeyRecord
> oneTimePreKeys
= generatePreKeys();
604 final IdentityKeyPair identityKeyPair
= getIdentityKeyPair();
605 SignedPreKeyRecord signedPreKeyRecord
= generateSignedPreKey(identityKeyPair
);
607 accountManager
.setPreKeys(identityKeyPair
.getPublicKey(), signedPreKeyRecord
, oneTimePreKeys
);
610 private SignalServiceMessageReceiver
createMessageReceiver() {
611 final ClientZkProfileOperations clientZkProfileOperations
= capabilities
.isGv2() ? ClientZkOperations
.create(
612 serviceConfiguration
).getProfileOperations() : null;
613 return new SignalServiceMessageReceiver(serviceConfiguration
,
615 account
.getUsername(),
616 account
.getPassword(),
617 account
.getDeviceId(),
618 account
.getSignalingKey(),
622 clientZkProfileOperations
);
625 private SignalServiceMessageReceiver
getOrCreateMessageReceiver() {
626 if (messageReceiver
== null) {
627 messageReceiver
= createMessageReceiver();
629 return messageReceiver
;
632 private SignalServiceMessagePipe
getOrCreateMessagePipe() {
633 if (messagePipe
== null) {
634 messagePipe
= getOrCreateMessageReceiver().createMessagePipe();
639 private SignalServiceMessagePipe
getOrCreateUnidentifiedMessagePipe() {
640 if (unidentifiedMessagePipe
== null) {
641 unidentifiedMessagePipe
= getOrCreateMessageReceiver().createUnidentifiedMessagePipe();
643 return unidentifiedMessagePipe
;
646 private SignalServiceMessageSender
createMessageSender() {
647 final ClientZkProfileOperations clientZkProfileOperations
= capabilities
.isGv2() ? ClientZkOperations
.create(
648 serviceConfiguration
).getProfileOperations() : null;
649 final ExecutorService executor
= null;
650 return new SignalServiceMessageSender(serviceConfiguration
,
652 account
.getUsername(),
653 account
.getPassword(),
654 account
.getDeviceId(),
655 account
.getSignalProtocolStore(),
657 account
.isMultiDevice(),
658 Optional
.fromNullable(messagePipe
),
659 Optional
.fromNullable(unidentifiedMessagePipe
),
661 clientZkProfileOperations
,
663 ServiceConfig
.MAX_ENVELOPE_SIZE
);
666 private SignalServiceProfile
getEncryptedRecipientProfile(SignalServiceAddress address
) throws IOException
{
667 return profileHelper
.retrieveProfileSync(address
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
670 private SignalProfile
getRecipientProfile(
671 SignalServiceAddress address
673 SignalProfileEntry profileEntry
= account
.getProfileStore().getProfileEntry(address
);
674 if (profileEntry
== null) {
677 long now
= new Date().getTime();
678 // Profiles are cache for 24h before retrieving them again
679 if (!profileEntry
.isRequestPending() && (
680 profileEntry
.getProfile() == null || now
- profileEntry
.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000
682 ProfileKey profileKey
= profileEntry
.getProfileKey();
683 profileEntry
.setRequestPending(true);
684 SignalProfile profile
;
686 profile
= retrieveRecipientProfile(address
, profileKey
);
687 } catch (IOException e
) {
688 logger
.warn("Failed to retrieve profile, ignoring: {}", e
.getMessage());
689 profileEntry
.setRequestPending(false);
692 profileEntry
.setRequestPending(false);
693 account
.getProfileStore()
694 .updateProfile(address
, profileKey
, now
, profile
, profileEntry
.getProfileKeyCredential());
697 return profileEntry
.getProfile();
700 private ProfileKeyCredential
getRecipientProfileKeyCredential(SignalServiceAddress address
) {
701 SignalProfileEntry profileEntry
= account
.getProfileStore().getProfileEntry(address
);
702 if (profileEntry
== null) {
705 if (profileEntry
.getProfileKeyCredential() == null) {
706 ProfileAndCredential profileAndCredential
;
708 profileAndCredential
= profileHelper
.retrieveProfileSync(address
,
709 SignalServiceProfile
.RequestType
.PROFILE_AND_CREDENTIAL
);
710 } catch (IOException e
) {
711 logger
.warn("Failed to retrieve profile key credential, ignoring: {}", e
.getMessage());
715 long now
= new Date().getTime();
716 final ProfileKeyCredential profileKeyCredential
= profileAndCredential
.getProfileKeyCredential().orNull();
717 final SignalProfile profile
= decryptProfile(address
,
718 profileEntry
.getProfileKey(),
719 profileAndCredential
.getProfile());
720 account
.getProfileStore()
721 .updateProfile(address
, profileEntry
.getProfileKey(), now
, profile
, profileKeyCredential
);
722 return profileKeyCredential
;
724 return profileEntry
.getProfileKeyCredential();
727 private SignalProfile
retrieveRecipientProfile(
728 SignalServiceAddress address
, ProfileKey profileKey
729 ) throws IOException
{
730 final SignalServiceProfile encryptedProfile
= getEncryptedRecipientProfile(address
);
732 return decryptProfile(address
, profileKey
, encryptedProfile
);
735 private SignalProfile
decryptProfile(
736 final SignalServiceAddress address
, final ProfileKey profileKey
, final SignalServiceProfile encryptedProfile
738 File avatarFile
= null;
740 avatarFile
= encryptedProfile
.getAvatar() == null
742 : retrieveProfileAvatar(address
, encryptedProfile
.getAvatar(), profileKey
);
743 } catch (Throwable e
) {
744 logger
.warn("Failed to retrieve profile avatar, ignoring: {}", e
.getMessage());
747 ProfileCipher profileCipher
= new ProfileCipher(profileKey
);
751 name
= encryptedProfile
.getName() == null
753 : new String(profileCipher
.decryptName(Base64
.decode(encryptedProfile
.getName())));
754 } catch (IOException e
) {
757 String unidentifiedAccess
;
759 unidentifiedAccess
= encryptedProfile
.getUnidentifiedAccess() == null
760 || !profileCipher
.verifyUnidentifiedAccess(Base64
.decode(encryptedProfile
.getUnidentifiedAccess()))
762 : encryptedProfile
.getUnidentifiedAccess();
763 } catch (IOException e
) {
764 unidentifiedAccess
= null;
766 return new SignalProfile(encryptedProfile
.getIdentityKey(),
770 encryptedProfile
.isUnrestrictedUnidentifiedAccess(),
771 encryptedProfile
.getCapabilities());
772 } catch (InvalidCiphertextException e
) {
777 private Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(GroupId groupId
) throws IOException
{
778 File file
= getGroupAvatarFile(groupId
);
779 if (!file
.exists()) {
780 return Optional
.absent();
783 return Optional
.of(AttachmentUtils
.createAttachment(file
));
786 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(String number
) throws IOException
{
787 File file
= getContactAvatarFile(number
);
788 if (!file
.exists()) {
789 return Optional
.absent();
792 return Optional
.of(AttachmentUtils
.createAttachment(file
));
795 private GroupInfo
getGroupForSending(GroupId groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
796 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
798 throw new GroupNotFoundException(groupId
);
800 if (!g
.isMember(account
.getSelfAddress())) {
801 throw new NotAGroupMemberException(groupId
, g
.getTitle());
806 private GroupInfo
getGroupForUpdating(GroupId groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
807 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
809 throw new GroupNotFoundException(groupId
);
811 if (!g
.isMember(account
.getSelfAddress()) && !g
.isPendingMember(account
.getSelfAddress())) {
812 throw new NotAGroupMemberException(groupId
, g
.getTitle());
817 public List
<GroupInfo
> getGroups() {
818 return account
.getGroupStore().getGroups();
821 public Pair
<Long
, List
<SendMessageResult
>> sendGroupMessage(
822 SignalServiceDataMessage
.Builder messageBuilder
, GroupId groupId
823 ) throws IOException
, GroupNotFoundException
, NotAGroupMemberException
{
824 final GroupInfo g
= getGroupForSending(groupId
);
826 GroupUtils
.setGroupContext(messageBuilder
, g
);
827 messageBuilder
.withExpiration(g
.getMessageExpirationTime());
829 return sendMessage(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
832 public Pair
<Long
, List
<SendMessageResult
>> sendGroupMessage(
833 String messageText
, List
<String
> attachments
, GroupId groupId
834 ) throws IOException
, GroupNotFoundException
, AttachmentInvalidException
, NotAGroupMemberException
{
835 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
836 .withBody(messageText
);
837 if (attachments
!= null) {
838 messageBuilder
.withAttachments(AttachmentUtils
.getSignalServiceAttachments(attachments
));
841 return sendGroupMessage(messageBuilder
, groupId
);
844 public Pair
<Long
, List
<SendMessageResult
>> sendGroupMessageReaction(
845 String emoji
, boolean remove
, String targetAuthor
, long targetSentTimestamp
, GroupId groupId
846 ) throws IOException
, InvalidNumberException
, NotAGroupMemberException
, GroupNotFoundException
{
847 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
,
849 canonicalizeAndResolveSignalServiceAddress(targetAuthor
),
850 targetSentTimestamp
);
851 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
852 .withReaction(reaction
);
854 return sendGroupMessage(messageBuilder
, groupId
);
857 public Pair
<Long
, List
<SendMessageResult
>> sendQuitGroupMessage(GroupId groupId
) throws GroupNotFoundException
, IOException
, NotAGroupMemberException
{
859 SignalServiceDataMessage
.Builder messageBuilder
;
861 final GroupInfo g
= getGroupForUpdating(groupId
);
862 if (g
instanceof GroupInfoV1
) {
863 GroupInfoV1 groupInfoV1
= (GroupInfoV1
) g
;
864 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
865 .withId(groupId
.serialize())
867 messageBuilder
= SignalServiceDataMessage
.newBuilder().asGroupMessage(group
);
868 groupInfoV1
.removeMember(account
.getSelfAddress());
869 account
.getGroupStore().updateGroup(groupInfoV1
);
871 final GroupInfoV2 groupInfoV2
= (GroupInfoV2
) g
;
872 final Pair
<DecryptedGroup
, GroupChange
> groupGroupChangePair
= groupHelper
.leaveGroup(groupInfoV2
);
873 groupInfoV2
.setGroup(groupGroupChangePair
.first());
874 messageBuilder
= getGroupUpdateMessageBuilder(groupInfoV2
, groupGroupChangePair
.second().toByteArray());
875 account
.getGroupStore().updateGroup(groupInfoV2
);
878 return sendMessage(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
881 private Pair
<GroupId
, List
<SendMessageResult
>> sendUpdateGroupMessage(
882 GroupId groupId
, String name
, Collection
<SignalServiceAddress
> members
, String avatarFile
883 ) throws IOException
, GroupNotFoundException
, AttachmentInvalidException
, NotAGroupMemberException
{
885 SignalServiceDataMessage
.Builder messageBuilder
;
886 if (groupId
== null) {
888 GroupInfoV2 gv2
= groupHelper
.createGroupV2(name
, members
, avatarFile
);
890 GroupInfoV1 gv1
= new GroupInfoV1(GroupIdV1
.createRandom());
891 gv1
.addMembers(List
.of(account
.getSelfAddress()));
892 updateGroupV1(gv1
, name
, members
, avatarFile
);
893 messageBuilder
= getGroupUpdateMessageBuilder(gv1
);
896 messageBuilder
= getGroupUpdateMessageBuilder(gv2
, null);
900 GroupInfo group
= getGroupForUpdating(groupId
);
901 if (group
instanceof GroupInfoV2
) {
902 final GroupInfoV2 groupInfoV2
= (GroupInfoV2
) group
;
904 Pair
<Long
, List
<SendMessageResult
>> result
= null;
905 if (groupInfoV2
.isPendingMember(getSelfAddress())) {
906 Pair
<DecryptedGroup
, GroupChange
> groupGroupChangePair
= groupHelper
.acceptInvite(groupInfoV2
);
907 result
= sendUpdateGroupMessage(groupInfoV2
,
908 groupGroupChangePair
.first(),
909 groupGroupChangePair
.second());
912 if (members
!= null) {
913 final Set
<SignalServiceAddress
> newMembers
= new HashSet
<>(members
);
914 newMembers
.removeAll(group
.getMembers()
916 .map(this::resolveSignalServiceAddress
)
917 .collect(Collectors
.toSet()));
918 if (newMembers
.size() > 0) {
919 Pair
<DecryptedGroup
, GroupChange
> groupGroupChangePair
= groupHelper
.updateGroupV2(groupInfoV2
,
921 result
= sendUpdateGroupMessage(groupInfoV2
,
922 groupGroupChangePair
.first(),
923 groupGroupChangePair
.second());
926 if (result
== null || name
!= null || avatarFile
!= null) {
927 Pair
<DecryptedGroup
, GroupChange
> groupGroupChangePair
= groupHelper
.updateGroupV2(groupInfoV2
,
930 result
= sendUpdateGroupMessage(groupInfoV2
,
931 groupGroupChangePair
.first(),
932 groupGroupChangePair
.second());
935 return new Pair
<>(group
.getGroupId(), result
.second());
937 GroupInfoV1 gv1
= (GroupInfoV1
) group
;
938 updateGroupV1(gv1
, name
, members
, avatarFile
);
939 messageBuilder
= getGroupUpdateMessageBuilder(gv1
);
944 account
.getGroupStore().updateGroup(g
);
946 final Pair
<Long
, List
<SendMessageResult
>> result
= sendMessage(messageBuilder
,
947 g
.getMembersIncludingPendingWithout(account
.getSelfAddress()));
948 return new Pair
<>(g
.getGroupId(), result
.second());
951 public Pair
<GroupId
, List
<SendMessageResult
>> joinGroup(
952 GroupInviteLinkUrl inviteLinkUrl
953 ) throws IOException
, GroupLinkNotActiveException
{
954 return sendJoinGroupMessage(inviteLinkUrl
);
957 private Pair
<GroupId
, List
<SendMessageResult
>> sendJoinGroupMessage(
958 GroupInviteLinkUrl inviteLinkUrl
959 ) throws IOException
, GroupLinkNotActiveException
{
960 final DecryptedGroupJoinInfo groupJoinInfo
= groupHelper
.getDecryptedGroupJoinInfo(inviteLinkUrl
.getGroupMasterKey(),
961 inviteLinkUrl
.getPassword());
962 final GroupChange groupChange
= groupHelper
.joinGroup(inviteLinkUrl
.getGroupMasterKey(),
963 inviteLinkUrl
.getPassword(),
965 final GroupInfoV2 group
= getOrMigrateGroup(inviteLinkUrl
.getGroupMasterKey(),
966 groupJoinInfo
.getRevision() + 1,
967 groupChange
.toByteArray());
969 if (group
.getGroup() == null) {
970 // Only requested member, can't send update to group members
971 return new Pair
<>(group
.getGroupId(), List
.of());
974 final Pair
<Long
, List
<SendMessageResult
>> result
= sendUpdateGroupMessage(group
, group
.getGroup(), groupChange
);
976 return new Pair
<>(group
.getGroupId(), result
.second());
979 private Pair
<Long
, List
<SendMessageResult
>> sendUpdateGroupMessage(
980 GroupInfoV2 group
, DecryptedGroup newDecryptedGroup
, GroupChange groupChange
981 ) throws IOException
{
982 group
.setGroup(newDecryptedGroup
);
983 final SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(group
,
984 groupChange
.toByteArray());
985 account
.getGroupStore().updateGroup(group
);
986 return sendMessage(messageBuilder
, group
.getMembersIncludingPendingWithout(account
.getSelfAddress()));
989 private void updateGroupV1(
992 final Collection
<SignalServiceAddress
> members
,
993 final String avatarFile
994 ) throws IOException
{
999 if (members
!= null) {
1000 final Set
<String
> newE164Members
= new HashSet
<>();
1001 for (SignalServiceAddress member
: members
) {
1002 if (g
.isMember(member
) || !member
.getNumber().isPresent()) {
1005 newE164Members
.add(member
.getNumber().get());
1008 final List
<ContactTokenDetails
> contacts
= accountManager
.getContacts(newE164Members
);
1009 if (contacts
.size() != newE164Members
.size()) {
1010 // Some of the new members are not registered on Signal
1011 for (ContactTokenDetails contact
: contacts
) {
1012 newE164Members
.remove(contact
.getNumber());
1014 throw new IOException("Failed to add members "
1015 + String
.join(", ", newE164Members
)
1016 + " to group: Not registered on Signal");
1019 g
.addMembers(members
);
1022 if (avatarFile
!= null) {
1023 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
1024 File aFile
= getGroupAvatarFile(g
.getGroupId());
1025 Files
.copy(Paths
.get(avatarFile
), aFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
1029 Pair
<Long
, List
<SendMessageResult
>> sendUpdateGroupMessage(
1030 GroupIdV1 groupId
, SignalServiceAddress recipient
1031 ) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
, AttachmentInvalidException
{
1033 GroupInfo group
= getGroupForSending(groupId
);
1034 if (!(group
instanceof GroupInfoV1
)) {
1035 throw new RuntimeException("Received an invalid group request for a v2 group!");
1037 g
= (GroupInfoV1
) group
;
1039 if (!g
.isMember(recipient
)) {
1040 throw new NotAGroupMemberException(groupId
, g
.name
);
1043 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
1045 // Send group message only to the recipient who requested it
1046 return sendMessage(messageBuilder
, List
.of(recipient
));
1049 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfoV1 g
) throws AttachmentInvalidException
{
1050 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
1051 .withId(g
.getGroupId().serialize())
1053 .withMembers(new ArrayList
<>(g
.getMembers()));
1055 File aFile
= getGroupAvatarFile(g
.getGroupId());
1056 if (aFile
.exists()) {
1058 group
.withAvatar(AttachmentUtils
.createAttachment(aFile
));
1059 } catch (IOException e
) {
1060 throw new AttachmentInvalidException(aFile
.toString(), e
);
1064 return SignalServiceDataMessage
.newBuilder()
1065 .asGroupMessage(group
.build())
1066 .withExpiration(g
.getMessageExpirationTime());
1069 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfoV2 g
, byte[] signedGroupChange
) {
1070 SignalServiceGroupV2
.Builder group
= SignalServiceGroupV2
.newBuilder(g
.getMasterKey())
1071 .withRevision(g
.getGroup().getRevision())
1072 .withSignedGroupChange(signedGroupChange
);
1073 return SignalServiceDataMessage
.newBuilder()
1074 .asGroupMessage(group
.build())
1075 .withExpiration(g
.getMessageExpirationTime());
1078 Pair
<Long
, List
<SendMessageResult
>> sendGroupInfoRequest(
1079 GroupIdV1 groupId
, SignalServiceAddress recipient
1080 ) throws IOException
{
1081 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
)
1082 .withId(groupId
.serialize());
1084 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
1085 .asGroupMessage(group
.build());
1087 // Send group info request message to the recipient who sent us a message with this groupId
1088 return sendMessage(messageBuilder
, List
.of(recipient
));
1092 SignalServiceAddress remoteAddress
, long messageId
1093 ) throws IOException
, UntrustedIdentityException
{
1094 SignalServiceReceiptMessage receiptMessage
= new SignalServiceReceiptMessage(SignalServiceReceiptMessage
.Type
.DELIVERY
,
1096 System
.currentTimeMillis());
1098 createMessageSender().sendReceipt(remoteAddress
,
1099 unidentifiedAccessHelper
.getAccessFor(remoteAddress
),
1103 public Pair
<Long
, List
<SendMessageResult
>> sendMessage(
1104 String messageText
, List
<String
> attachments
, List
<String
> recipients
1105 ) throws IOException
, AttachmentInvalidException
, InvalidNumberException
{
1106 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
1107 .withBody(messageText
);
1108 if (attachments
!= null) {
1109 List
<SignalServiceAttachment
> attachmentStreams
= AttachmentUtils
.getSignalServiceAttachments(attachments
);
1111 // Upload attachments here, so we only upload once even for multiple recipients
1112 SignalServiceMessageSender messageSender
= createMessageSender();
1113 List
<SignalServiceAttachment
> attachmentPointers
= new ArrayList
<>(attachmentStreams
.size());
1114 for (SignalServiceAttachment attachment
: attachmentStreams
) {
1115 if (attachment
.isStream()) {
1116 attachmentPointers
.add(messageSender
.uploadAttachment(attachment
.asStream()));
1117 } else if (attachment
.isPointer()) {
1118 attachmentPointers
.add(attachment
.asPointer());
1122 messageBuilder
.withAttachments(attachmentPointers
);
1124 return sendMessage(messageBuilder
, getSignalServiceAddresses(recipients
));
1127 public Pair
<Long
, List
<SendMessageResult
>> sendMessageReaction(
1128 String emoji
, boolean remove
, String targetAuthor
, long targetSentTimestamp
, List
<String
> recipients
1129 ) throws IOException
, InvalidNumberException
{
1130 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
,
1132 canonicalizeAndResolveSignalServiceAddress(targetAuthor
),
1133 targetSentTimestamp
);
1134 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
1135 .withReaction(reaction
);
1136 return sendMessage(messageBuilder
, getSignalServiceAddresses(recipients
));
1139 public Pair
<Long
, List
<SendMessageResult
>> sendEndSessionMessage(List
<String
> recipients
) throws IOException
, InvalidNumberException
{
1140 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().asEndSessionMessage();
1142 final Collection
<SignalServiceAddress
> signalServiceAddresses
= getSignalServiceAddresses(recipients
);
1144 return sendMessage(messageBuilder
, signalServiceAddresses
);
1145 } catch (Exception e
) {
1146 for (SignalServiceAddress address
: signalServiceAddresses
) {
1147 handleEndSession(address
);
1154 public String
getContactName(String number
) throws InvalidNumberException
{
1155 ContactInfo contact
= account
.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number
));
1156 if (contact
== null) {
1159 return contact
.name
;
1163 public void setContactName(String number
, String name
) throws InvalidNumberException
{
1164 final SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(number
);
1165 ContactInfo contact
= account
.getContactStore().getContact(address
);
1166 if (contact
== null) {
1167 contact
= new ContactInfo(address
);
1169 contact
.name
= name
;
1170 account
.getContactStore().updateContact(contact
);
1174 public void setContactBlocked(String number
, boolean blocked
) throws InvalidNumberException
{
1175 setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number
), blocked
);
1178 private void setContactBlocked(SignalServiceAddress address
, boolean blocked
) {
1179 ContactInfo contact
= account
.getContactStore().getContact(address
);
1180 if (contact
== null) {
1181 contact
= new ContactInfo(address
);
1183 contact
.blocked
= blocked
;
1184 account
.getContactStore().updateContact(contact
);
1188 public void setGroupBlocked(final GroupId groupId
, final boolean blocked
) throws GroupNotFoundException
{
1189 GroupInfo group
= getGroup(groupId
);
1190 if (group
== null) {
1191 throw new GroupNotFoundException(groupId
);
1194 group
.setBlocked(blocked
);
1195 account
.getGroupStore().updateGroup(group
);
1199 public Pair
<GroupId
, List
<SendMessageResult
>> updateGroup(
1200 GroupId groupId
, String name
, List
<String
> members
, String avatar
1201 ) throws IOException
, GroupNotFoundException
, AttachmentInvalidException
, InvalidNumberException
, NotAGroupMemberException
{
1202 return sendUpdateGroupMessage(groupId
,
1204 members
== null ?
null : getSignalServiceAddresses(members
),
1209 * Change the expiration timer for a contact
1211 public void setExpirationTimer(SignalServiceAddress address
, int messageExpirationTimer
) throws IOException
{
1212 ContactInfo contact
= account
.getContactStore().getContact(address
);
1213 contact
.messageExpirationTime
= messageExpirationTimer
;
1214 account
.getContactStore().updateContact(contact
);
1215 sendExpirationTimerUpdate(address
);
1219 private void sendExpirationTimerUpdate(SignalServiceAddress address
) throws IOException
{
1220 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
1221 .asExpirationUpdate();
1222 sendMessage(messageBuilder
, List
.of(address
));
1226 * Change the expiration timer for a contact
1228 public void setExpirationTimer(
1229 String number
, int messageExpirationTimer
1230 ) throws IOException
, InvalidNumberException
{
1231 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(number
);
1232 setExpirationTimer(address
, messageExpirationTimer
);
1236 * Change the expiration timer for a group
1238 public void setExpirationTimer(GroupId groupId
, int messageExpirationTimer
) {
1239 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
1240 if (g
instanceof GroupInfoV1
) {
1241 GroupInfoV1 groupInfoV1
= (GroupInfoV1
) g
;
1242 groupInfoV1
.messageExpirationTime
= messageExpirationTimer
;
1243 account
.getGroupStore().updateGroup(groupInfoV1
);
1245 throw new RuntimeException("TODO Not implemented!");
1250 * Upload the sticker pack from path.
1252 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
1253 * @return if successful, returns the URL to install the sticker pack in the signal app
1255 public String
uploadStickerPack(File path
) throws IOException
, StickerPackInvalidException
{
1256 SignalServiceStickerManifestUpload manifest
= getSignalServiceStickerManifestUpload(path
);
1258 SignalServiceMessageSender messageSender
= createMessageSender();
1260 byte[] packKey
= KeyUtils
.createStickerUploadKey();
1261 String packId
= messageSender
.uploadStickerManifest(manifest
, packKey
);
1263 Sticker sticker
= new Sticker(Hex
.fromStringCondensed(packId
), packKey
);
1264 account
.getStickerStore().updateSticker(sticker
);
1268 return new URI("https",
1271 "pack_id=" + URLEncoder
.encode(packId
, StandardCharsets
.UTF_8
) + "&pack_key=" + URLEncoder
.encode(
1272 Hex
.toStringCondensed(packKey
),
1273 StandardCharsets
.UTF_8
)).toString();
1274 } catch (URISyntaxException e
) {
1275 throw new AssertionError(e
);
1279 private SignalServiceStickerManifestUpload
getSignalServiceStickerManifestUpload(
1281 ) throws IOException
, StickerPackInvalidException
{
1283 String rootPath
= null;
1285 if (file
.getName().endsWith(".zip")) {
1286 zip
= new ZipFile(file
);
1287 } else if (file
.getName().equals("manifest.json")) {
1288 rootPath
= file
.getParent();
1290 throw new StickerPackInvalidException("Could not find manifest.json");
1293 JsonStickerPack pack
= parseStickerPack(rootPath
, zip
);
1295 if (pack
.stickers
== null) {
1296 throw new StickerPackInvalidException("Must set a 'stickers' field.");
1299 if (pack
.stickers
.isEmpty()) {
1300 throw new StickerPackInvalidException("Must include stickers.");
1303 List
<StickerInfo
> stickers
= new ArrayList
<>(pack
.stickers
.size());
1304 for (JsonStickerPack
.JsonSticker sticker
: pack
.stickers
) {
1305 if (sticker
.file
== null) {
1306 throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
1309 Pair
<InputStream
, Long
> data
;
1311 data
= getInputStreamAndLength(rootPath
, zip
, sticker
.file
);
1312 } catch (IOException ignored
) {
1313 throw new StickerPackInvalidException("Could not find find " + sticker
.file
);
1316 String contentType
= Utils
.getFileMimeType(new File(sticker
.file
), null);
1317 StickerInfo stickerInfo
= new StickerInfo(data
.first(),
1319 Optional
.fromNullable(sticker
.emoji
).or(""),
1321 stickers
.add(stickerInfo
);
1324 StickerInfo cover
= null;
1325 if (pack
.cover
!= null) {
1326 if (pack
.cover
.file
== null) {
1327 throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
1330 Pair
<InputStream
, Long
> data
;
1332 data
= getInputStreamAndLength(rootPath
, zip
, pack
.cover
.file
);
1333 } catch (IOException ignored
) {
1334 throw new StickerPackInvalidException("Could not find find " + pack
.cover
.file
);
1337 String contentType
= Utils
.getFileMimeType(new File(pack
.cover
.file
), null);
1338 cover
= new StickerInfo(data
.first(),
1340 Optional
.fromNullable(pack
.cover
.emoji
).or(""),
1344 return new SignalServiceStickerManifestUpload(pack
.title
, pack
.author
, cover
, stickers
);
1347 private static JsonStickerPack
parseStickerPack(String rootPath
, ZipFile zip
) throws IOException
{
1348 InputStream inputStream
;
1350 inputStream
= zip
.getInputStream(zip
.getEntry("manifest.json"));
1352 inputStream
= new FileInputStream((new File(rootPath
, "manifest.json")));
1354 return new ObjectMapper().readValue(inputStream
, JsonStickerPack
.class);
1357 private static Pair
<InputStream
, Long
> getInputStreamAndLength(
1358 final String rootPath
, final ZipFile zip
, final String subfile
1359 ) throws IOException
{
1361 final ZipEntry entry
= zip
.getEntry(subfile
);
1362 return new Pair
<>(zip
.getInputStream(entry
), entry
.getSize());
1364 final File file
= new File(rootPath
, subfile
);
1365 return new Pair
<>(new FileInputStream(file
), file
.length());
1369 void requestSyncGroups() throws IOException
{
1370 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
1371 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
)
1373 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1375 sendSyncMessage(message
);
1376 } catch (UntrustedIdentityException e
) {
1377 e
.printStackTrace();
1381 void requestSyncContacts() throws IOException
{
1382 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
1383 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
)
1385 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1387 sendSyncMessage(message
);
1388 } catch (UntrustedIdentityException e
) {
1389 e
.printStackTrace();
1393 void requestSyncBlocked() throws IOException
{
1394 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
1395 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
)
1397 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1399 sendSyncMessage(message
);
1400 } catch (UntrustedIdentityException e
) {
1401 e
.printStackTrace();
1405 void requestSyncConfiguration() throws IOException
{
1406 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
1407 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
)
1409 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1411 sendSyncMessage(message
);
1412 } catch (UntrustedIdentityException e
) {
1413 e
.printStackTrace();
1417 private byte[] getSenderCertificate() {
1418 // TODO support UUID capable sender certificates
1419 // byte[] certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy();
1422 certificate
= accountManager
.getSenderCertificate();
1423 } catch (IOException e
) {
1424 logger
.warn("Failed to get sender certificate, ignoring: {}", e
.getMessage());
1427 // TODO cache for a day
1431 private void sendSyncMessage(SignalServiceSyncMessage message
) throws IOException
, UntrustedIdentityException
{
1432 SignalServiceMessageSender messageSender
= createMessageSender();
1434 messageSender
.sendMessage(message
, unidentifiedAccessHelper
.getAccessForSync());
1435 } catch (UntrustedIdentityException e
) {
1436 account
.getSignalProtocolStore()
1437 .saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()),
1439 TrustLevel
.UNTRUSTED
);
1444 private Collection
<SignalServiceAddress
> getSignalServiceAddresses(Collection
<String
> numbers
) throws InvalidNumberException
{
1445 final Set
<SignalServiceAddress
> signalServiceAddresses
= new HashSet
<>(numbers
.size());
1446 final Set
<SignalServiceAddress
> missingUuids
= new HashSet
<>();
1448 for (String number
: numbers
) {
1449 final SignalServiceAddress resolvedAddress
= canonicalizeAndResolveSignalServiceAddress(number
);
1450 if (resolvedAddress
.getUuid().isPresent()) {
1451 signalServiceAddresses
.add(resolvedAddress
);
1453 missingUuids
.add(resolvedAddress
);
1457 Map
<String
, UUID
> registeredUsers
;
1459 registeredUsers
= accountManager
.getRegisteredUsers(getIasKeyStore(),
1460 missingUuids
.stream().map(a
-> a
.getNumber().get()).collect(Collectors
.toSet()),
1462 } catch (IOException
| Quote
.InvalidQuoteFormatException
| UnauthenticatedQuoteException
| SignatureException
| UnauthenticatedResponseException e
) {
1463 logger
.warn("Failed to resolve uuids from server, ignoring: {}", e
.getMessage());
1464 registeredUsers
= new HashMap
<>();
1467 for (SignalServiceAddress address
: missingUuids
) {
1468 final String number
= address
.getNumber().get();
1469 if (registeredUsers
.containsKey(number
)) {
1470 final SignalServiceAddress newAddress
= resolveSignalServiceAddress(new SignalServiceAddress(
1471 registeredUsers
.get(number
),
1473 signalServiceAddresses
.add(newAddress
);
1475 signalServiceAddresses
.add(address
);
1479 return signalServiceAddresses
;
1482 private Pair
<Long
, List
<SendMessageResult
>> sendMessage(
1483 SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
1484 ) throws IOException
{
1485 recipients
= recipients
.stream().map(this::resolveSignalServiceAddress
).collect(Collectors
.toSet());
1486 final long timestamp
= System
.currentTimeMillis();
1487 messageBuilder
.withTimestamp(timestamp
);
1488 getOrCreateMessagePipe();
1489 getOrCreateUnidentifiedMessagePipe();
1490 SignalServiceDataMessage message
= null;
1492 message
= messageBuilder
.build();
1493 if (message
.getGroupContext().isPresent()) {
1495 SignalServiceMessageSender messageSender
= createMessageSender();
1496 final boolean isRecipientUpdate
= false;
1497 List
<SendMessageResult
> result
= messageSender
.sendMessage(new ArrayList
<>(recipients
),
1498 unidentifiedAccessHelper
.getAccessFor(recipients
),
1501 for (SendMessageResult r
: result
) {
1502 if (r
.getIdentityFailure() != null) {
1503 account
.getSignalProtocolStore()
1504 .saveIdentity(r
.getAddress(),
1505 r
.getIdentityFailure().getIdentityKey(),
1506 TrustLevel
.UNTRUSTED
);
1509 return new Pair
<>(timestamp
, result
);
1510 } catch (UntrustedIdentityException e
) {
1511 account
.getSignalProtocolStore()
1512 .saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()),
1514 TrustLevel
.UNTRUSTED
);
1515 return new Pair
<>(timestamp
, List
.of());
1518 // Send to all individually, so sync messages are sent correctly
1519 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1520 for (SignalServiceAddress address
: recipients
) {
1521 ContactInfo contact
= account
.getContactStore().getContact(address
);
1522 if (contact
!= null) {
1523 messageBuilder
.withExpiration(contact
.messageExpirationTime
);
1524 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
1526 messageBuilder
.withExpiration(0);
1527 messageBuilder
.withProfileKey(null);
1529 message
= messageBuilder
.build();
1530 if (address
.matches(account
.getSelfAddress())) {
1531 results
.add(sendSelfMessage(message
));
1533 results
.add(sendMessage(address
, message
));
1536 return new Pair
<>(timestamp
, results
);
1539 if (message
!= null && message
.isEndSession()) {
1540 for (SignalServiceAddress recipient
: recipients
) {
1541 handleEndSession(recipient
);
1548 private SendMessageResult
sendSelfMessage(SignalServiceDataMessage message
) throws IOException
{
1549 SignalServiceMessageSender messageSender
= createMessageSender();
1551 SignalServiceAddress recipient
= account
.getSelfAddress();
1553 final Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= unidentifiedAccessHelper
.getAccessFor(recipient
);
1554 SentTranscriptMessage transcript
= new SentTranscriptMessage(Optional
.of(recipient
),
1555 message
.getTimestamp(),
1557 message
.getExpiresInSeconds(),
1558 Map
.of(recipient
, unidentifiedAccess
.isPresent()),
1560 SignalServiceSyncMessage syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
1563 long startTime
= System
.currentTimeMillis();
1564 messageSender
.sendMessage(syncMessage
, unidentifiedAccess
);
1565 return SendMessageResult
.success(recipient
,
1566 unidentifiedAccess
.isPresent(),
1568 System
.currentTimeMillis() - startTime
);
1569 } catch (UntrustedIdentityException e
) {
1570 account
.getSignalProtocolStore()
1571 .saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()),
1573 TrustLevel
.UNTRUSTED
);
1574 return SendMessageResult
.identityFailure(recipient
, e
.getIdentityKey());
1578 private SendMessageResult
sendMessage(
1579 SignalServiceAddress address
, SignalServiceDataMessage message
1580 ) throws IOException
{
1581 SignalServiceMessageSender messageSender
= createMessageSender();
1584 return messageSender
.sendMessage(address
, unidentifiedAccessHelper
.getAccessFor(address
), message
);
1585 } catch (UntrustedIdentityException e
) {
1586 account
.getSignalProtocolStore()
1587 .saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()),
1589 TrustLevel
.UNTRUSTED
);
1590 return SendMessageResult
.identityFailure(address
, e
.getIdentityKey());
1594 private SignalServiceContent
decryptMessage(SignalServiceEnvelope envelope
) throws InvalidMetadataMessageException
, ProtocolInvalidMessageException
, ProtocolDuplicateMessageException
, ProtocolLegacyMessageException
, ProtocolInvalidKeyIdException
, InvalidMetadataVersionException
, ProtocolInvalidVersionException
, ProtocolNoSessionException
, ProtocolInvalidKeyException
, SelfSendException
, UnsupportedDataMessageException
, org
.whispersystems
.libsignal
.UntrustedIdentityException
{
1595 SignalServiceCipher cipher
= new SignalServiceCipher(account
.getSelfAddress(),
1596 account
.getSignalProtocolStore(),
1597 certificateValidator
);
1599 return cipher
.decrypt(envelope
);
1600 } catch (ProtocolUntrustedIdentityException e
) {
1601 if (e
.getCause() instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
) {
1602 org
.whispersystems
.libsignal
.UntrustedIdentityException identityException
= (org
.whispersystems
.libsignal
.UntrustedIdentityException
) e
1604 account
.getSignalProtocolStore()
1605 .saveIdentity(resolveSignalServiceAddress(identityException
.getName()),
1606 identityException
.getUntrustedIdentity(),
1607 TrustLevel
.UNTRUSTED
);
1608 throw identityException
;
1610 throw new AssertionError(e
);
1614 private void handleEndSession(SignalServiceAddress source
) {
1615 account
.getSignalProtocolStore().deleteAllSessions(source
);
1618 private static int currentTimeDays() {
1619 return (int) TimeUnit
.MILLISECONDS
.toDays(System
.currentTimeMillis());
1622 private GroupsV2AuthorizationString
getGroupAuthForToday(
1623 final GroupSecretParams groupSecretParams
1624 ) throws IOException
{
1625 final int today
= currentTimeDays();
1626 // Returns credentials for the next 7 days
1627 final HashMap
<Integer
, AuthCredentialResponse
> credentials
= groupsV2Api
.getCredentials(today
);
1628 // TODO cache credentials until they expire
1629 AuthCredentialResponse authCredentialResponse
= credentials
.get(today
);
1631 return groupsV2Api
.getGroupsV2AuthorizationString(account
.getUuid(),
1634 authCredentialResponse
);
1635 } catch (VerificationFailedException e
) {
1636 throw new IOException(e
);
1640 private List
<HandleAction
> handleSignalServiceDataMessage(
1641 SignalServiceDataMessage message
,
1643 SignalServiceAddress source
,
1644 SignalServiceAddress destination
,
1645 boolean ignoreAttachments
1647 List
<HandleAction
> actions
= new ArrayList
<>();
1648 if (message
.getGroupContext().isPresent()) {
1649 if (message
.getGroupContext().get().getGroupV1().isPresent()) {
1650 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1651 GroupIdV1 groupId
= GroupId
.v1(groupInfo
.getGroupId());
1652 GroupInfo group
= account
.getGroupStore().getGroup(groupId
);
1653 if (group
== null || group
instanceof GroupInfoV1
) {
1654 GroupInfoV1 groupV1
= (GroupInfoV1
) group
;
1655 switch (groupInfo
.getType()) {
1657 if (groupV1
== null) {
1658 groupV1
= new GroupInfoV1(groupId
);
1661 if (groupInfo
.getAvatar().isPresent()) {
1662 SignalServiceAttachment avatar
= groupInfo
.getAvatar().get();
1663 if (avatar
.isPointer()) {
1665 retrieveGroupAvatarAttachment(avatar
.asPointer(), groupV1
.getGroupId());
1666 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1667 logger
.warn("Failed to retrieve avatar for group {}, ignoring: {}",
1674 if (groupInfo
.getName().isPresent()) {
1675 groupV1
.name
= groupInfo
.getName().get();
1678 if (groupInfo
.getMembers().isPresent()) {
1679 groupV1
.addMembers(groupInfo
.getMembers()
1682 .map(this::resolveSignalServiceAddress
)
1683 .collect(Collectors
.toSet()));
1686 account
.getGroupStore().updateGroup(groupV1
);
1690 if (groupV1
== null && !isSync
) {
1691 actions
.add(new SendGroupInfoRequestAction(source
, groupId
));
1695 if (groupV1
!= null) {
1696 groupV1
.removeMember(source
);
1697 account
.getGroupStore().updateGroup(groupV1
);
1702 if (groupV1
!= null && !isSync
) {
1703 actions
.add(new SendGroupUpdateAction(source
, groupV1
.getGroupId()));
1708 // Received a group v1 message for a v2 group
1711 if (message
.getGroupContext().get().getGroupV2().isPresent()) {
1712 final SignalServiceGroupV2 groupContext
= message
.getGroupContext().get().getGroupV2().get();
1713 final GroupMasterKey groupMasterKey
= groupContext
.getMasterKey();
1715 getOrMigrateGroup(groupMasterKey
,
1716 groupContext
.getRevision(),
1717 groupContext
.hasSignedGroupChange() ? groupContext
.getSignedGroupChange() : null);
1721 final SignalServiceAddress conversationPartnerAddress
= isSync ? destination
: source
;
1722 if (conversationPartnerAddress
!= null && message
.isEndSession()) {
1723 handleEndSession(conversationPartnerAddress
);
1725 if (message
.isExpirationUpdate() || message
.getBody().isPresent()) {
1726 if (message
.getGroupContext().isPresent()) {
1727 if (message
.getGroupContext().get().getGroupV1().isPresent()) {
1728 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1729 GroupInfoV1 group
= account
.getGroupStore().getOrCreateGroupV1(GroupId
.v1(groupInfo
.getGroupId()));
1730 if (group
!= null) {
1731 if (group
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1732 group
.messageExpirationTime
= message
.getExpiresInSeconds();
1733 account
.getGroupStore().updateGroup(group
);
1736 } else if (message
.getGroupContext().get().getGroupV2().isPresent()) {
1737 // disappearing message timer already stored in the DecryptedGroup
1739 } else if (conversationPartnerAddress
!= null) {
1740 ContactInfo contact
= account
.getContactStore().getContact(conversationPartnerAddress
);
1741 if (contact
== null) {
1742 contact
= new ContactInfo(conversationPartnerAddress
);
1744 if (contact
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1745 contact
.messageExpirationTime
= message
.getExpiresInSeconds();
1746 account
.getContactStore().updateContact(contact
);
1750 if (message
.getAttachments().isPresent() && !ignoreAttachments
) {
1751 for (SignalServiceAttachment attachment
: message
.getAttachments().get()) {
1752 if (attachment
.isPointer()) {
1754 retrieveAttachment(attachment
.asPointer());
1755 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1756 logger
.warn("Failed to retrieve attachment ({}), ignoring: {}",
1757 attachment
.asPointer().getRemoteId(),
1763 if (message
.getProfileKey().isPresent() && message
.getProfileKey().get().length
== 32) {
1764 final ProfileKey profileKey
;
1766 profileKey
= new ProfileKey(message
.getProfileKey().get());
1767 } catch (InvalidInputException e
) {
1768 throw new AssertionError(e
);
1770 if (source
.matches(account
.getSelfAddress())) {
1771 this.account
.setProfileKey(profileKey
);
1773 this.account
.getProfileStore().storeProfileKey(source
, profileKey
);
1775 if (message
.getPreviews().isPresent()) {
1776 final List
<SignalServiceDataMessage
.Preview
> previews
= message
.getPreviews().get();
1777 for (SignalServiceDataMessage
.Preview preview
: previews
) {
1778 if (preview
.getImage().isPresent() && preview
.getImage().get().isPointer()) {
1779 SignalServiceAttachmentPointer attachment
= preview
.getImage().get().asPointer();
1781 retrieveAttachment(attachment
);
1782 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1783 logger
.warn("Failed to retrieve preview image ({}), ignoring: {}",
1784 attachment
.getRemoteId(),
1790 if (message
.getQuote().isPresent()) {
1791 final SignalServiceDataMessage
.Quote quote
= message
.getQuote().get();
1793 for (SignalServiceDataMessage
.Quote
.QuotedAttachment quotedAttachment
: quote
.getAttachments()) {
1794 final SignalServiceAttachment attachment
= quotedAttachment
.getThumbnail();
1795 if (attachment
!= null && attachment
.isPointer()) {
1797 retrieveAttachment(attachment
.asPointer());
1798 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1799 logger
.warn("Failed to retrieve quote attachment thumbnail ({}), ignoring: {}",
1800 attachment
.asPointer().getRemoteId(),
1806 if (message
.getSticker().isPresent()) {
1807 final SignalServiceDataMessage
.Sticker messageSticker
= message
.getSticker().get();
1808 Sticker sticker
= account
.getStickerStore().getSticker(messageSticker
.getPackId());
1809 if (sticker
== null) {
1810 sticker
= new Sticker(messageSticker
.getPackId(), messageSticker
.getPackKey());
1811 account
.getStickerStore().updateSticker(sticker
);
1817 private GroupInfoV2
getOrMigrateGroup(
1818 final GroupMasterKey groupMasterKey
, final int revision
, final byte[] signedGroupChange
1820 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
1822 GroupIdV2 groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
1823 GroupInfo groupInfo
= account
.getGroupStore().getGroup(groupId
);
1824 final GroupInfoV2 groupInfoV2
;
1825 if (groupInfo
instanceof GroupInfoV1
) {
1826 // Received a v2 group message for a v1 group, we need to locally migrate the group
1827 account
.getGroupStore().deleteGroup(groupInfo
.getGroupId());
1828 groupInfoV2
= new GroupInfoV2(groupId
, groupMasterKey
);
1829 logger
.info("Locally migrated group {} to group v2, id: {}",
1830 groupInfo
.getGroupId().toBase64(),
1831 groupInfoV2
.getGroupId().toBase64());
1832 } else if (groupInfo
instanceof GroupInfoV2
) {
1833 groupInfoV2
= (GroupInfoV2
) groupInfo
;
1835 groupInfoV2
= new GroupInfoV2(groupId
, groupMasterKey
);
1838 if (groupInfoV2
.getGroup() == null || groupInfoV2
.getGroup().getRevision() < revision
) {
1839 DecryptedGroup group
= null;
1840 if (signedGroupChange
!= null
1841 && groupInfoV2
.getGroup() != null
1842 && groupInfoV2
.getGroup().getRevision() + 1 == revision
) {
1843 group
= groupHelper
.getUpdatedDecryptedGroup(groupInfoV2
.getGroup(), signedGroupChange
, groupMasterKey
);
1845 if (group
== null) {
1846 group
= groupHelper
.getDecryptedGroup(groupSecretParams
);
1848 if (group
!= null) {
1849 storeProfileKeysFromMembers(group
);
1850 final String avatar
= group
.getAvatar();
1851 if (avatar
!= null && !avatar
.isEmpty()) {
1853 retrieveGroupAvatar(groupId
, groupSecretParams
, avatar
);
1854 } catch (IOException e
) {
1855 logger
.warn("Failed to download group avatar, ignoring: {}", e
.getMessage());
1859 groupInfoV2
.setGroup(group
);
1860 account
.getGroupStore().updateGroup(groupInfoV2
);
1866 private void storeProfileKeysFromMembers(final DecryptedGroup group
) {
1867 for (DecryptedMember member
: group
.getMembersList()) {
1868 final SignalServiceAddress address
= resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil
.parseOrThrow(
1869 member
.getUuid().toByteArray()), null));
1871 account
.getProfileStore()
1872 .storeProfileKey(address
, new ProfileKey(member
.getProfileKey().toByteArray()));
1873 } catch (InvalidInputException ignored
) {
1878 private void retryFailedReceivedMessages(
1879 ReceiveMessageHandler handler
, boolean ignoreAttachments
1881 final File cachePath
= getMessageCachePath();
1882 if (!cachePath
.exists()) {
1885 for (final File dir
: Objects
.requireNonNull(cachePath
.listFiles())) {
1886 if (!dir
.isDirectory()) {
1887 retryFailedReceivedMessage(handler
, ignoreAttachments
, dir
);
1891 for (final File fileEntry
: Objects
.requireNonNull(dir
.listFiles())) {
1892 if (!fileEntry
.isFile()) {
1895 retryFailedReceivedMessage(handler
, ignoreAttachments
, fileEntry
);
1897 // Try to delete directory if empty
1902 private void retryFailedReceivedMessage(
1903 final ReceiveMessageHandler handler
, final boolean ignoreAttachments
, final File fileEntry
1905 SignalServiceEnvelope envelope
;
1907 envelope
= MessageCacheUtils
.loadEnvelope(fileEntry
);
1908 if (envelope
== null) {
1911 } catch (IOException e
) {
1912 e
.printStackTrace();
1915 SignalServiceContent content
= null;
1916 if (!envelope
.isReceipt()) {
1918 content
= decryptMessage(envelope
);
1919 } catch (org
.whispersystems
.libsignal
.UntrustedIdentityException e
) {
1921 } catch (Exception er
) {
1922 // All other errors are not recoverable, so delete the cached message
1924 Files
.delete(fileEntry
.toPath());
1925 } catch (IOException e
) {
1926 logger
.warn("Failed to delete cached message file “{}”, ignoring: {}", fileEntry
, e
.getMessage());
1930 List
<HandleAction
> actions
= handleMessage(envelope
, content
, ignoreAttachments
);
1931 for (HandleAction action
: actions
) {
1933 action
.execute(this);
1934 } catch (Throwable e
) {
1935 e
.printStackTrace();
1940 handler
.handleMessage(envelope
, content
, null);
1942 Files
.delete(fileEntry
.toPath());
1943 } catch (IOException e
) {
1944 logger
.warn("Failed to delete cached message file “{}”, ignoring: {}", fileEntry
, e
.getMessage());
1948 public void receiveMessages(
1951 boolean returnOnTimeout
,
1952 boolean ignoreAttachments
,
1953 ReceiveMessageHandler handler
1954 ) throws IOException
{
1955 retryFailedReceivedMessages(handler
, ignoreAttachments
);
1957 Set
<HandleAction
> queuedActions
= null;
1959 getOrCreateMessagePipe();
1961 boolean hasCaughtUpWithOldMessages
= false;
1964 SignalServiceEnvelope envelope
;
1965 SignalServiceContent content
= null;
1966 Exception exception
= null;
1967 final long now
= new Date().getTime();
1969 Optional
<SignalServiceEnvelope
> result
= messagePipe
.readOrEmpty(timeout
, unit
, envelope1
-> {
1970 // store message on disk, before acknowledging receipt to the server
1972 String source
= envelope1
.getSourceE164().isPresent() ? envelope1
.getSourceE164().get() : "";
1973 File cacheFile
= getMessageCacheFile(source
, now
, envelope1
.getTimestamp());
1974 MessageCacheUtils
.storeEnvelope(envelope1
, cacheFile
);
1975 } catch (IOException e
) {
1976 logger
.warn("Failed to store encrypted message in disk cache, ignoring: {}", e
.getMessage());
1979 if (result
.isPresent()) {
1980 envelope
= result
.get();
1982 // Received indicator that server queue is empty
1983 hasCaughtUpWithOldMessages
= true;
1985 if (queuedActions
!= null) {
1986 for (HandleAction action
: queuedActions
) {
1988 action
.execute(this);
1989 } catch (Throwable e
) {
1990 e
.printStackTrace();
1994 queuedActions
.clear();
1995 queuedActions
= null;
1998 // Continue to wait another timeout for new messages
2001 } catch (TimeoutException e
) {
2002 if (returnOnTimeout
) return;
2004 } catch (InvalidVersionException e
) {
2005 logger
.warn("Error while receiving messages, ignoring: {}", e
.getMessage());
2009 if (envelope
.hasSource()) {
2010 // Store uuid if we don't have it already
2011 SignalServiceAddress source
= envelope
.getSourceAddress();
2012 resolveSignalServiceAddress(source
);
2014 if (!envelope
.isReceipt()) {
2016 content
= decryptMessage(envelope
);
2017 } catch (Exception e
) {
2020 List
<HandleAction
> actions
= handleMessage(envelope
, content
, ignoreAttachments
);
2021 if (hasCaughtUpWithOldMessages
) {
2022 for (HandleAction action
: actions
) {
2024 action
.execute(this);
2025 } catch (Throwable e
) {
2026 e
.printStackTrace();
2030 if (queuedActions
== null) {
2031 queuedActions
= new HashSet
<>();
2033 queuedActions
.addAll(actions
);
2037 if (!isMessageBlocked(envelope
, content
)) {
2038 handler
.handleMessage(envelope
, content
, exception
);
2040 if (!(exception
instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
)) {
2041 File cacheFile
= null;
2043 String source
= envelope
.getSourceE164().isPresent() ? envelope
.getSourceE164().get() : "";
2044 cacheFile
= getMessageCacheFile(source
, now
, envelope
.getTimestamp());
2045 Files
.delete(cacheFile
.toPath());
2046 // Try to delete directory if empty
2047 getMessageCachePath().delete();
2048 } catch (IOException e
) {
2049 logger
.warn("Failed to delete cached message file “{}”, ignoring: {}", cacheFile
, e
.getMessage());
2055 private boolean isMessageBlocked(
2056 SignalServiceEnvelope envelope
, SignalServiceContent content
2058 SignalServiceAddress source
;
2059 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
2060 source
= envelope
.getSourceAddress();
2061 } else if (content
!= null) {
2062 source
= content
.getSender();
2066 ContactInfo sourceContact
= account
.getContactStore().getContact(source
);
2067 if (sourceContact
!= null && sourceContact
.blocked
) {
2071 if (content
!= null && content
.getDataMessage().isPresent()) {
2072 SignalServiceDataMessage message
= content
.getDataMessage().get();
2073 if (message
.getGroupContext().isPresent()) {
2074 if (message
.getGroupContext().get().getGroupV1().isPresent()) {
2075 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
2076 if (groupInfo
.getType() != SignalServiceGroup
.Type
.DELIVER
) {
2080 GroupId groupId
= GroupUtils
.getGroupId(message
.getGroupContext().get());
2081 GroupInfo group
= account
.getGroupStore().getGroup(groupId
);
2082 if (group
!= null && group
.isBlocked()) {
2090 private List
<HandleAction
> handleMessage(
2091 SignalServiceEnvelope envelope
, SignalServiceContent content
, boolean ignoreAttachments
2093 List
<HandleAction
> actions
= new ArrayList
<>();
2094 if (content
!= null) {
2095 final SignalServiceAddress sender
;
2096 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
2097 sender
= envelope
.getSourceAddress();
2099 sender
= content
.getSender();
2101 // Store uuid if we don't have it already
2102 resolveSignalServiceAddress(sender
);
2104 if (content
.getDataMessage().isPresent()) {
2105 SignalServiceDataMessage message
= content
.getDataMessage().get();
2107 if (content
.isNeedsReceipt()) {
2108 actions
.add(new SendReceiptAction(sender
, message
.getTimestamp()));
2111 actions
.addAll(handleSignalServiceDataMessage(message
,
2114 account
.getSelfAddress(),
2115 ignoreAttachments
));
2117 if (content
.getSyncMessage().isPresent()) {
2118 account
.setMultiDevice(true);
2119 SignalServiceSyncMessage syncMessage
= content
.getSyncMessage().get();
2120 if (syncMessage
.getSent().isPresent()) {
2121 SentTranscriptMessage message
= syncMessage
.getSent().get();
2122 final SignalServiceAddress destination
= message
.getDestination().orNull();
2123 actions
.addAll(handleSignalServiceDataMessage(message
.getMessage(),
2127 ignoreAttachments
));
2129 if (syncMessage
.getRequest().isPresent()) {
2130 RequestMessage rm
= syncMessage
.getRequest().get();
2131 if (rm
.isContactsRequest()) {
2132 actions
.add(SendSyncContactsAction
.create());
2134 if (rm
.isGroupsRequest()) {
2135 actions
.add(SendSyncGroupsAction
.create());
2137 if (rm
.isBlockedListRequest()) {
2138 actions
.add(SendSyncBlockedListAction
.create());
2140 // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest();
2142 if (syncMessage
.getGroups().isPresent()) {
2143 File tmpFile
= null;
2145 tmpFile
= IOUtils
.createTempFile();
2146 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(syncMessage
.getGroups()
2148 .asPointer(), tmpFile
)) {
2149 DeviceGroupsInputStream s
= new DeviceGroupsInputStream(attachmentAsStream
);
2151 while ((g
= s
.read()) != null) {
2152 GroupInfoV1 syncGroup
= account
.getGroupStore()
2153 .getOrCreateGroupV1(GroupId
.v1(g
.getId()));
2154 if (syncGroup
!= null) {
2155 if (g
.getName().isPresent()) {
2156 syncGroup
.name
= g
.getName().get();
2158 syncGroup
.addMembers(g
.getMembers()
2160 .map(this::resolveSignalServiceAddress
)
2161 .collect(Collectors
.toSet()));
2162 if (!g
.isActive()) {
2163 syncGroup
.removeMember(account
.getSelfAddress());
2165 // Add ourself to the member set as it's marked as active
2166 syncGroup
.addMembers(List
.of(account
.getSelfAddress()));
2168 syncGroup
.blocked
= g
.isBlocked();
2169 if (g
.getColor().isPresent()) {
2170 syncGroup
.color
= g
.getColor().get();
2173 if (g
.getAvatar().isPresent()) {
2174 retrieveGroupAvatarAttachment(g
.getAvatar().get(), syncGroup
.getGroupId());
2176 syncGroup
.inboxPosition
= g
.getInboxPosition().orNull();
2177 syncGroup
.archived
= g
.isArchived();
2178 account
.getGroupStore().updateGroup(syncGroup
);
2182 } catch (Exception e
) {
2183 logger
.warn("Failed to handle received sync groups “{}”, ignoring: {}",
2186 e
.printStackTrace();
2188 if (tmpFile
!= null) {
2190 Files
.delete(tmpFile
.toPath());
2191 } catch (IOException e
) {
2192 logger
.warn("Failed to delete received groups temp file “{}”, ignoring: {}",
2199 if (syncMessage
.getBlockedList().isPresent()) {
2200 final BlockedListMessage blockedListMessage
= syncMessage
.getBlockedList().get();
2201 for (SignalServiceAddress address
: blockedListMessage
.getAddresses()) {
2202 setContactBlocked(resolveSignalServiceAddress(address
), true);
2204 for (GroupId groupId
: blockedListMessage
.getGroupIds()
2206 .map(GroupId
::unknownVersion
)
2207 .collect(Collectors
.toSet())) {
2209 setGroupBlocked(groupId
, true);
2210 } catch (GroupNotFoundException e
) {
2211 logger
.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}",
2212 groupId
.toBase64());
2216 if (syncMessage
.getContacts().isPresent()) {
2217 File tmpFile
= null;
2219 tmpFile
= IOUtils
.createTempFile();
2220 final ContactsMessage contactsMessage
= syncMessage
.getContacts().get();
2221 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(contactsMessage
.getContactsStream()
2222 .asPointer(), tmpFile
)) {
2223 DeviceContactsInputStream s
= new DeviceContactsInputStream(attachmentAsStream
);
2224 if (contactsMessage
.isComplete()) {
2225 account
.getContactStore().clear();
2228 while ((c
= s
.read()) != null) {
2229 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
2230 account
.setProfileKey(c
.getProfileKey().get());
2232 final SignalServiceAddress address
= resolveSignalServiceAddress(c
.getAddress());
2233 ContactInfo contact
= account
.getContactStore().getContact(address
);
2234 if (contact
== null) {
2235 contact
= new ContactInfo(address
);
2237 if (c
.getName().isPresent()) {
2238 contact
.name
= c
.getName().get();
2240 if (c
.getColor().isPresent()) {
2241 contact
.color
= c
.getColor().get();
2243 if (c
.getProfileKey().isPresent()) {
2244 account
.getProfileStore().storeProfileKey(address
, c
.getProfileKey().get());
2246 if (c
.getVerified().isPresent()) {
2247 final VerifiedMessage verifiedMessage
= c
.getVerified().get();
2248 account
.getSignalProtocolStore()
2249 .setIdentityTrustLevel(verifiedMessage
.getDestination(),
2250 verifiedMessage
.getIdentityKey(),
2251 TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
2253 if (c
.getExpirationTimer().isPresent()) {
2254 contact
.messageExpirationTime
= c
.getExpirationTimer().get();
2256 contact
.blocked
= c
.isBlocked();
2257 contact
.inboxPosition
= c
.getInboxPosition().orNull();
2258 contact
.archived
= c
.isArchived();
2259 account
.getContactStore().updateContact(contact
);
2261 if (c
.getAvatar().isPresent()) {
2262 retrieveContactAvatarAttachment(c
.getAvatar().get(), contact
.number
);
2266 } catch (Exception e
) {
2267 e
.printStackTrace();
2269 if (tmpFile
!= null) {
2271 Files
.delete(tmpFile
.toPath());
2272 } catch (IOException e
) {
2273 logger
.warn("Failed to delete received contacts temp file “{}”, ignoring: {}",
2280 if (syncMessage
.getVerified().isPresent()) {
2281 final VerifiedMessage verifiedMessage
= syncMessage
.getVerified().get();
2282 account
.getSignalProtocolStore()
2283 .setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage
.getDestination()),
2284 verifiedMessage
.getIdentityKey(),
2285 TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
2287 if (syncMessage
.getStickerPackOperations().isPresent()) {
2288 final List
<StickerPackOperationMessage
> stickerPackOperationMessages
= syncMessage
.getStickerPackOperations()
2290 for (StickerPackOperationMessage m
: stickerPackOperationMessages
) {
2291 if (!m
.getPackId().isPresent()) {
2294 Sticker sticker
= account
.getStickerStore().getSticker(m
.getPackId().get());
2295 if (sticker
== null) {
2296 if (!m
.getPackKey().isPresent()) {
2299 sticker
= new Sticker(m
.getPackId().get(), m
.getPackKey().get());
2301 sticker
.setInstalled(!m
.getType().isPresent()
2302 || m
.getType().get() == StickerPackOperationMessage
.Type
.INSTALL
);
2303 account
.getStickerStore().updateSticker(sticker
);
2306 if (syncMessage
.getConfiguration().isPresent()) {
2314 private File
getContactAvatarFile(String number
) {
2315 return new File(pathConfig
.getAvatarsPath(), "contact-" + number
);
2318 private File
retrieveContactAvatarAttachment(
2319 SignalServiceAttachment attachment
, String number
2320 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2321 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
2322 if (attachment
.isPointer()) {
2323 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
2324 return retrieveAttachment(pointer
, getContactAvatarFile(number
), false);
2326 SignalServiceAttachmentStream stream
= attachment
.asStream();
2327 return AttachmentUtils
.retrieveAttachment(stream
, getContactAvatarFile(number
));
2331 private File
getGroupAvatarFile(GroupId groupId
) {
2332 return new File(pathConfig
.getAvatarsPath(), "group-" + groupId
.toBase64().replace("/", "_"));
2335 private File
retrieveGroupAvatarAttachment(
2336 SignalServiceAttachment attachment
, GroupId groupId
2337 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2338 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
2339 if (attachment
.isPointer()) {
2340 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
2341 return retrieveAttachment(pointer
, getGroupAvatarFile(groupId
), false);
2343 SignalServiceAttachmentStream stream
= attachment
.asStream();
2344 return AttachmentUtils
.retrieveAttachment(stream
, getGroupAvatarFile(groupId
));
2348 private File
retrieveGroupAvatar(
2349 GroupId groupId
, GroupSecretParams groupSecretParams
, String cdnKey
2350 ) throws IOException
{
2351 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
2352 SignalServiceMessageReceiver receiver
= getOrCreateMessageReceiver();
2353 File outputFile
= getGroupAvatarFile(groupId
);
2354 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
2356 File tmpFile
= IOUtils
.createTempFile();
2357 tmpFile
.deleteOnExit();
2358 try (InputStream input
= receiver
.retrieveGroupsV2ProfileAvatar(cdnKey
,
2360 ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
)) {
2361 byte[] encryptedData
= IOUtils
.readFully(input
);
2363 byte[] decryptedData
= groupOperations
.decryptAvatar(encryptedData
);
2364 try (OutputStream output
= new FileOutputStream(outputFile
)) {
2365 output
.write(decryptedData
);
2369 Files
.delete(tmpFile
.toPath());
2370 } catch (IOException e
) {
2371 logger
.warn("Failed to delete received group avatar temp file “{}”, ignoring: {}",
2379 private File
getProfileAvatarFile(SignalServiceAddress address
) {
2380 return new File(pathConfig
.getAvatarsPath(), "profile-" + address
.getLegacyIdentifier());
2383 private File
retrieveProfileAvatar(
2384 SignalServiceAddress address
, String avatarPath
, ProfileKey profileKey
2385 ) throws IOException
{
2386 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
2387 SignalServiceMessageReceiver receiver
= getOrCreateMessageReceiver();
2388 File outputFile
= getProfileAvatarFile(address
);
2390 File tmpFile
= IOUtils
.createTempFile();
2391 try (InputStream input
= receiver
.retrieveProfileAvatar(avatarPath
,
2394 ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
)) {
2395 // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
2396 IOUtils
.copyStreamToFile(input
, outputFile
, (int) ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
);
2399 Files
.delete(tmpFile
.toPath());
2400 } catch (IOException e
) {
2401 logger
.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}",
2409 public File
getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId
) {
2410 return new File(pathConfig
.getAttachmentsPath(), attachmentId
.toString());
2413 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2414 IOUtils
.createPrivateDirectories(pathConfig
.getAttachmentsPath());
2415 return retrieveAttachment(pointer
, getAttachmentFile(pointer
.getRemoteId()), true);
2418 private File
retrieveAttachment(
2419 SignalServiceAttachmentPointer pointer
, File outputFile
, boolean storePreview
2420 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2421 if (storePreview
&& pointer
.getPreview().isPresent()) {
2422 File previewFile
= new File(outputFile
+ ".preview");
2423 try (OutputStream output
= new FileOutputStream(previewFile
)) {
2424 byte[] preview
= pointer
.getPreview().get();
2425 output
.write(preview
, 0, preview
.length
);
2426 } catch (FileNotFoundException e
) {
2427 e
.printStackTrace();
2432 final SignalServiceMessageReceiver messageReceiver
= getOrCreateMessageReceiver();
2434 File tmpFile
= IOUtils
.createTempFile();
2435 try (InputStream input
= messageReceiver
.retrieveAttachment(pointer
,
2437 ServiceConfig
.MAX_ATTACHMENT_SIZE
)) {
2438 IOUtils
.copyStreamToFile(input
, outputFile
);
2441 Files
.delete(tmpFile
.toPath());
2442 } catch (IOException e
) {
2443 logger
.warn("Failed to delete received attachment temp file “{}”, ignoring: {}",
2451 private InputStream
retrieveAttachmentAsStream(
2452 SignalServiceAttachmentPointer pointer
, File tmpFile
2453 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2454 final SignalServiceMessageReceiver messageReceiver
= getOrCreateMessageReceiver();
2455 return messageReceiver
.retrieveAttachment(pointer
, tmpFile
, ServiceConfig
.MAX_ATTACHMENT_SIZE
);
2458 void sendGroups() throws IOException
, UntrustedIdentityException
{
2459 File groupsFile
= IOUtils
.createTempFile();
2462 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
2463 DeviceGroupsOutputStream out
= new DeviceGroupsOutputStream(fos
);
2464 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
2465 if (record instanceof GroupInfoV1
) {
2466 GroupInfoV1 groupInfo
= (GroupInfoV1
) record;
2467 out
.write(new DeviceGroup(groupInfo
.getGroupId().serialize(),
2468 Optional
.fromNullable(groupInfo
.name
),
2469 new ArrayList
<>(groupInfo
.getMembers()),
2470 createGroupAvatarAttachment(groupInfo
.getGroupId()),
2471 groupInfo
.isMember(account
.getSelfAddress()),
2472 Optional
.of(groupInfo
.messageExpirationTime
),
2473 Optional
.fromNullable(groupInfo
.color
),
2475 Optional
.fromNullable(groupInfo
.inboxPosition
),
2476 groupInfo
.archived
));
2481 if (groupsFile
.exists() && groupsFile
.length() > 0) {
2482 try (FileInputStream groupsFileStream
= new FileInputStream(groupsFile
)) {
2483 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
2484 .withStream(groupsFileStream
)
2485 .withContentType("application/octet-stream")
2486 .withLength(groupsFile
.length())
2489 sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
2494 Files
.delete(groupsFile
.toPath());
2495 } catch (IOException e
) {
2496 logger
.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile
, e
.getMessage());
2501 public void sendContacts() throws IOException
, UntrustedIdentityException
{
2502 File contactsFile
= IOUtils
.createTempFile();
2505 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
2506 DeviceContactsOutputStream out
= new DeviceContactsOutputStream(fos
);
2507 for (ContactInfo
record : account
.getContactStore().getContacts()) {
2508 VerifiedMessage verifiedMessage
= null;
2509 IdentityInfo currentIdentity
= account
.getSignalProtocolStore().getIdentity(record.getAddress());
2510 if (currentIdentity
!= null) {
2511 verifiedMessage
= new VerifiedMessage(record.getAddress(),
2512 currentIdentity
.getIdentityKey(),
2513 currentIdentity
.getTrustLevel().toVerifiedState(),
2514 currentIdentity
.getDateAdded().getTime());
2517 ProfileKey profileKey
= account
.getProfileStore().getProfileKey(record.getAddress());
2518 out
.write(new DeviceContact(record.getAddress(),
2519 Optional
.fromNullable(record.name
),
2520 createContactAvatarAttachment(record.number
),
2521 Optional
.fromNullable(record.color
),
2522 Optional
.fromNullable(verifiedMessage
),
2523 Optional
.fromNullable(profileKey
),
2525 Optional
.of(record.messageExpirationTime
),
2526 Optional
.fromNullable(record.inboxPosition
),
2530 if (account
.getProfileKey() != null) {
2531 // Send our own profile key as well
2532 out
.write(new DeviceContact(account
.getSelfAddress(),
2537 Optional
.of(account
.getProfileKey()),
2545 if (contactsFile
.exists() && contactsFile
.length() > 0) {
2546 try (FileInputStream contactsFileStream
= new FileInputStream(contactsFile
)) {
2547 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
2548 .withStream(contactsFileStream
)
2549 .withContentType("application/octet-stream")
2550 .withLength(contactsFile
.length())
2553 sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
, true)));
2558 Files
.delete(contactsFile
.toPath());
2559 } catch (IOException e
) {
2560 logger
.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile
, e
.getMessage());
2565 void sendBlockedList() throws IOException
, UntrustedIdentityException
{
2566 List
<SignalServiceAddress
> addresses
= new ArrayList
<>();
2567 for (ContactInfo
record : account
.getContactStore().getContacts()) {
2568 if (record.blocked
) {
2569 addresses
.add(record.getAddress());
2572 List
<byte[]> groupIds
= new ArrayList
<>();
2573 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
2574 if (record.isBlocked()) {
2575 groupIds
.add(record.getGroupId().serialize());
2578 sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
2581 private void sendVerifiedMessage(
2582 SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
2583 ) throws IOException
, UntrustedIdentityException
{
2584 VerifiedMessage verifiedMessage
= new VerifiedMessage(destination
,
2586 trustLevel
.toVerifiedState(),
2587 System
.currentTimeMillis());
2588 sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
2591 public List
<ContactInfo
> getContacts() {
2592 return account
.getContactStore().getContacts();
2595 public ContactInfo
getContact(String number
) {
2596 return account
.getContactStore().getContact(Utils
.getSignalServiceAddressFromIdentifier(number
));
2599 public GroupInfo
getGroup(GroupId groupId
) {
2600 return account
.getGroupStore().getGroup(groupId
);
2603 public List
<IdentityInfo
> getIdentities() {
2604 return account
.getSignalProtocolStore().getIdentities();
2607 public List
<IdentityInfo
> getIdentities(String number
) throws InvalidNumberException
{
2608 return account
.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number
));
2612 * Trust this the identity with this fingerprint
2614 * @param name username of the identity
2615 * @param fingerprint Fingerprint
2617 public boolean trustIdentityVerified(String name
, byte[] fingerprint
) throws InvalidNumberException
{
2618 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
2619 List
<IdentityInfo
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2623 for (IdentityInfo id
: ids
) {
2624 if (!Arrays
.equals(id
.getIdentityKey().serialize(), fingerprint
)) {
2628 account
.getSignalProtocolStore()
2629 .setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2631 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2632 } catch (IOException
| UntrustedIdentityException e
) {
2633 e
.printStackTrace();
2642 * Trust this the identity with this safety number
2644 * @param name username of the identity
2645 * @param safetyNumber Safety number
2647 public boolean trustIdentityVerifiedSafetyNumber(String name
, String safetyNumber
) throws InvalidNumberException
{
2648 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
2649 List
<IdentityInfo
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2653 for (IdentityInfo id
: ids
) {
2654 if (!safetyNumber
.equals(computeSafetyNumber(address
, id
.getIdentityKey()))) {
2658 account
.getSignalProtocolStore()
2659 .setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2661 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2662 } catch (IOException
| UntrustedIdentityException e
) {
2663 e
.printStackTrace();
2672 * Trust all keys of this identity without verification
2674 * @param name username of the identity
2676 public boolean trustIdentityAllKeys(String name
) {
2677 SignalServiceAddress address
= resolveSignalServiceAddress(name
);
2678 List
<IdentityInfo
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2682 for (IdentityInfo id
: ids
) {
2683 if (id
.getTrustLevel() == TrustLevel
.UNTRUSTED
) {
2684 account
.getSignalProtocolStore()
2685 .setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2687 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2688 } catch (IOException
| UntrustedIdentityException e
) {
2689 e
.printStackTrace();
2697 public String
computeSafetyNumber(
2698 SignalServiceAddress theirAddress
, IdentityKey theirIdentityKey
2700 return Utils
.computeSafetyNumber(ServiceConfig
.capabilities
.isUuid(),
2701 account
.getSelfAddress(),
2702 getIdentityKeyPair().getPublicKey(),
2707 void saveAccount() {
2711 public SignalServiceAddress
canonicalizeAndResolveSignalServiceAddress(String identifier
) throws InvalidNumberException
{
2712 String canonicalizedNumber
= UuidUtil
.isUuid(identifier
)
2714 : PhoneNumberFormatter
.formatNumber(identifier
, account
.getUsername());
2715 return resolveSignalServiceAddress(canonicalizedNumber
);
2718 public SignalServiceAddress
resolveSignalServiceAddress(String identifier
) {
2719 SignalServiceAddress address
= Utils
.getSignalServiceAddressFromIdentifier(identifier
);
2721 return resolveSignalServiceAddress(address
);
2724 public SignalServiceAddress
resolveSignalServiceAddress(SignalServiceAddress address
) {
2725 if (address
.matches(account
.getSelfAddress())) {
2726 return account
.getSelfAddress();
2729 return account
.getRecipientStore().resolveServiceAddress(address
);
2733 public void close() throws IOException
{
2734 if (messagePipe
!= null) {
2735 messagePipe
.shutdown();
2739 if (unidentifiedMessagePipe
!= null) {
2740 unidentifiedMessagePipe
.shutdown();
2741 unidentifiedMessagePipe
= null;
2747 public interface ReceiveMessageHandler
{
2749 void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent decryptedContent
, Throwable e
);