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
.storage
.SignalAccount
;
22 import org
.asamk
.signal
.storage
.contacts
.ContactInfo
;
23 import org
.asamk
.signal
.storage
.groups
.GroupInfo
;
24 import org
.asamk
.signal
.storage
.groups
.GroupInfoV1
;
25 import org
.asamk
.signal
.storage
.groups
.GroupInfoV2
;
26 import org
.asamk
.signal
.storage
.profiles
.SignalProfile
;
27 import org
.asamk
.signal
.storage
.profiles
.SignalProfileEntry
;
28 import org
.asamk
.signal
.storage
.protocol
.JsonIdentityKeyStore
;
29 import org
.asamk
.signal
.util
.IOUtils
;
30 import org
.asamk
.signal
.util
.Util
;
31 import org
.signal
.libsignal
.metadata
.InvalidMetadataMessageException
;
32 import org
.signal
.libsignal
.metadata
.InvalidMetadataVersionException
;
33 import org
.signal
.libsignal
.metadata
.ProtocolDuplicateMessageException
;
34 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyException
;
35 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyIdException
;
36 import org
.signal
.libsignal
.metadata
.ProtocolInvalidMessageException
;
37 import org
.signal
.libsignal
.metadata
.ProtocolInvalidVersionException
;
38 import org
.signal
.libsignal
.metadata
.ProtocolLegacyMessageException
;
39 import org
.signal
.libsignal
.metadata
.ProtocolNoSessionException
;
40 import org
.signal
.libsignal
.metadata
.ProtocolUntrustedIdentityException
;
41 import org
.signal
.libsignal
.metadata
.SelfSendException
;
42 import org
.signal
.libsignal
.metadata
.certificate
.InvalidCertificateException
;
43 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroup
;
44 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedMember
;
45 import org
.signal
.zkgroup
.InvalidInputException
;
46 import org
.signal
.zkgroup
.VerificationFailedException
;
47 import org
.signal
.zkgroup
.auth
.AuthCredentialResponse
;
48 import org
.signal
.zkgroup
.groups
.GroupMasterKey
;
49 import org
.signal
.zkgroup
.groups
.GroupSecretParams
;
50 import org
.signal
.zkgroup
.profiles
.ClientZkProfileOperations
;
51 import org
.signal
.zkgroup
.profiles
.ProfileKey
;
52 import org
.whispersystems
.libsignal
.IdentityKey
;
53 import org
.whispersystems
.libsignal
.IdentityKeyPair
;
54 import org
.whispersystems
.libsignal
.InvalidKeyException
;
55 import org
.whispersystems
.libsignal
.InvalidMessageException
;
56 import org
.whispersystems
.libsignal
.InvalidVersionException
;
57 import org
.whispersystems
.libsignal
.ecc
.Curve
;
58 import org
.whispersystems
.libsignal
.ecc
.ECKeyPair
;
59 import org
.whispersystems
.libsignal
.ecc
.ECPublicKey
;
60 import org
.whispersystems
.libsignal
.state
.PreKeyRecord
;
61 import org
.whispersystems
.libsignal
.state
.SignedPreKeyRecord
;
62 import org
.whispersystems
.libsignal
.util
.KeyHelper
;
63 import org
.whispersystems
.libsignal
.util
.Medium
;
64 import org
.whispersystems
.libsignal
.util
.Pair
;
65 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
66 import org
.whispersystems
.signalservice
.api
.SignalServiceAccountManager
;
67 import org
.whispersystems
.signalservice
.api
.SignalServiceMessagePipe
;
68 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageReceiver
;
69 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageSender
;
70 import org
.whispersystems
.signalservice
.api
.crypto
.InvalidCiphertextException
;
71 import org
.whispersystems
.signalservice
.api
.crypto
.ProfileCipher
;
72 import org
.whispersystems
.signalservice
.api
.crypto
.SignalServiceCipher
;
73 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccess
;
74 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccessPair
;
75 import org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException
;
76 import org
.whispersystems
.signalservice
.api
.groupsv2
.ClientZkOperations
;
77 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Api
;
78 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2AuthorizationString
;
79 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Operations
;
80 import org
.whispersystems
.signalservice
.api
.groupsv2
.InvalidGroupStateException
;
81 import org
.whispersystems
.signalservice
.api
.messages
.SendMessageResult
;
82 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachment
;
83 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentPointer
;
84 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentRemoteId
;
85 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentStream
;
86 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceContent
;
87 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceDataMessage
;
88 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceEnvelope
;
89 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroup
;
90 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroupV2
;
91 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceReceiptMessage
;
92 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceStickerManifestUpload
;
93 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceStickerManifestUpload
.StickerInfo
;
94 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.BlockedListMessage
;
95 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ContactsMessage
;
96 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContact
;
97 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsInputStream
;
98 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsOutputStream
;
99 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroup
;
100 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsInputStream
;
101 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsOutputStream
;
102 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceInfo
;
103 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.RequestMessage
;
104 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SentTranscriptMessage
;
105 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SignalServiceSyncMessage
;
106 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.VerifiedMessage
;
107 import org
.whispersystems
.signalservice
.api
.profiles
.SignalServiceProfile
;
108 import org
.whispersystems
.signalservice
.api
.push
.ContactTokenDetails
;
109 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
110 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.MissingConfigurationException
;
111 import org
.whispersystems
.signalservice
.api
.util
.InvalidNumberException
;
112 import org
.whispersystems
.signalservice
.api
.util
.SleepTimer
;
113 import org
.whispersystems
.signalservice
.api
.util
.StreamDetails
;
114 import org
.whispersystems
.signalservice
.api
.util
.UptimeSleepTimer
;
115 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
116 import org
.whispersystems
.signalservice
.internal
.configuration
.SignalServiceConfiguration
;
117 import org
.whispersystems
.signalservice
.internal
.push
.SignalServiceProtos
;
118 import org
.whispersystems
.signalservice
.internal
.push
.UnsupportedDataMessageException
;
119 import org
.whispersystems
.signalservice
.internal
.push
.VerifyAccountResponse
;
120 import org
.whispersystems
.signalservice
.internal
.util
.DynamicCredentialsProvider
;
121 import org
.whispersystems
.signalservice
.internal
.util
.Hex
;
122 import org
.whispersystems
.util
.Base64
;
124 import java
.io
.Closeable
;
126 import java
.io
.FileInputStream
;
127 import java
.io
.FileNotFoundException
;
128 import java
.io
.FileOutputStream
;
129 import java
.io
.IOException
;
130 import java
.io
.InputStream
;
131 import java
.io
.OutputStream
;
133 import java
.net
.URISyntaxException
;
134 import java
.net
.URLEncoder
;
135 import java
.nio
.charset
.StandardCharsets
;
136 import java
.nio
.file
.Files
;
137 import java
.nio
.file
.Paths
;
138 import java
.nio
.file
.StandardCopyOption
;
139 import java
.util
.ArrayList
;
140 import java
.util
.Arrays
;
141 import java
.util
.Collection
;
142 import java
.util
.Collections
;
143 import java
.util
.Date
;
144 import java
.util
.HashMap
;
145 import java
.util
.HashSet
;
146 import java
.util
.List
;
147 import java
.util
.Locale
;
148 import java
.util
.Objects
;
149 import java
.util
.Set
;
150 import java
.util
.UUID
;
151 import java
.util
.concurrent
.ExecutionException
;
152 import java
.util
.concurrent
.ExecutorService
;
153 import java
.util
.concurrent
.TimeUnit
;
154 import java
.util
.concurrent
.TimeoutException
;
155 import java
.util
.stream
.Collectors
;
156 import java
.util
.zip
.ZipEntry
;
157 import java
.util
.zip
.ZipFile
;
159 import static org
.asamk
.signal
.manager
.ServiceConfig
.capabilities
;
161 public class Manager
implements Closeable
{
163 private final SleepTimer timer
= new UptimeSleepTimer();
164 private final SignalServiceConfiguration serviceConfiguration
;
165 private final String userAgent
;
167 private final SignalAccount account
;
168 private final PathConfig pathConfig
;
169 private SignalServiceAccountManager accountManager
;
170 private GroupsV2Api groupsV2Api
;
171 private SignalServiceMessagePipe messagePipe
= null;
172 private SignalServiceMessagePipe unidentifiedMessagePipe
= null;
173 private final boolean discoverableByPhoneNumber
= true;
175 public Manager(SignalAccount account
, PathConfig pathConfig
, SignalServiceConfiguration serviceConfiguration
, String userAgent
) {
176 this.account
= account
;
177 this.pathConfig
= pathConfig
;
178 this.serviceConfiguration
= serviceConfiguration
;
179 this.userAgent
= userAgent
;
180 this.accountManager
= createSignalServiceAccountManager();
181 this.groupsV2Api
= accountManager
.getGroupsV2Api();
183 this.account
.setResolver(this::resolveSignalServiceAddress
);
186 public String
getUsername() {
187 return account
.getUsername();
190 public SignalServiceAddress
getSelfAddress() {
191 return account
.getSelfAddress();
194 private SignalServiceAccountManager
createSignalServiceAccountManager() {
195 GroupsV2Operations groupsV2Operations
= capabilities
.isGv2()
196 ?
new GroupsV2Operations(ClientZkOperations
.create(serviceConfiguration
))
199 return new SignalServiceAccountManager(serviceConfiguration
,
200 new DynamicCredentialsProvider(account
.getUuid(), account
.getUsername(), account
.getPassword(), null, account
.getDeviceId()),
206 private IdentityKeyPair
getIdentityKeyPair() {
207 return account
.getSignalProtocolStore().getIdentityKeyPair();
210 public int getDeviceId() {
211 return account
.getDeviceId();
214 private String
getMessageCachePath() {
215 return pathConfig
.getDataPath() + "/" + account
.getUsername() + ".d/msg-cache";
218 private String
getMessageCachePath(String sender
) {
219 if (sender
== null || sender
.isEmpty()) {
220 return getMessageCachePath();
223 return getMessageCachePath() + "/" + sender
.replace("/", "_");
226 private File
getMessageCacheFile(String sender
, long now
, long timestamp
) throws IOException
{
227 String cachePath
= getMessageCachePath(sender
);
228 IOUtils
.createPrivateDirectories(cachePath
);
229 return new File(cachePath
+ "/" + now
+ "_" + timestamp
);
232 public static Manager
init(String username
, String settingsPath
, SignalServiceConfiguration serviceConfiguration
, String userAgent
) throws IOException
{
233 PathConfig pathConfig
= PathConfig
.createDefault(settingsPath
);
235 if (!SignalAccount
.userExists(pathConfig
.getDataPath(), username
)) {
236 IdentityKeyPair identityKey
= KeyHelper
.generateIdentityKeyPair();
237 int registrationId
= KeyHelper
.generateRegistrationId(false);
239 ProfileKey profileKey
= KeyUtils
.createProfileKey();
240 SignalAccount account
= SignalAccount
.create(pathConfig
.getDataPath(), username
, identityKey
, registrationId
, profileKey
);
243 return new Manager(account
, pathConfig
, serviceConfiguration
, userAgent
);
246 SignalAccount account
= SignalAccount
.load(pathConfig
.getDataPath(), username
);
248 Manager m
= new Manager(account
, pathConfig
, serviceConfiguration
, userAgent
);
250 m
.migrateLegacyConfigs();
251 m
.updateAccountAttributes();
256 private void migrateLegacyConfigs() {
257 if (account
.getProfileKey() == null) {
258 // Old config file, creating new profile key
259 account
.setProfileKey(KeyUtils
.createProfileKey());
262 // Store profile keys only in profile store
263 for (ContactInfo contact
: account
.getContactStore().getContacts()) {
264 String profileKeyString
= contact
.profileKey
;
265 if (profileKeyString
== null) {
268 final ProfileKey profileKey
;
270 profileKey
= new ProfileKey(Base64
.decode(profileKeyString
));
271 } catch (InvalidInputException
| IOException e
) {
274 contact
.profileKey
= null;
275 account
.getProfileStore().storeProfileKey(contact
.getAddress(), profileKey
);
279 public void checkAccountState() throws IOException
{
280 if (account
.isRegistered()) {
281 if (accountManager
.getPreKeysCount() < ServiceConfig
.PREKEY_MINIMUM_COUNT
) {
285 if (account
.getUuid() == null) {
286 account
.setUuid(accountManager
.getOwnUuid());
292 public boolean isRegistered() {
293 return account
.isRegistered();
296 public void register(boolean voiceVerification
, String captcha
) throws IOException
{
297 account
.setPassword(KeyUtils
.createPassword());
299 // Resetting UUID, because registering doesn't work otherwise
300 account
.setUuid(null);
301 accountManager
= createSignalServiceAccountManager();
302 this.groupsV2Api
= accountManager
.getGroupsV2Api();
304 if (voiceVerification
) {
305 accountManager
.requestVoiceVerificationCode(Locale
.getDefault(), Optional
.fromNullable(captcha
), Optional
.absent());
307 accountManager
.requestSmsVerificationCode(false, Optional
.fromNullable(captcha
), Optional
.absent());
310 account
.setRegistered(false);
314 public void updateAccountAttributes() throws IOException
{
315 accountManager
.setAccountAttributes(account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, account
.getRegistrationLockPin(), account
.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities
, discoverableByPhoneNumber
);
318 public void setProfile(String name
, File avatar
) throws IOException
{
319 try (final StreamDetails streamDetails
= avatar
== null ?
null : Utils
.createStreamDetailsFromFile(avatar
)) {
320 accountManager
.setVersionedProfile(account
.getUuid(), account
.getProfileKey(), name
, streamDetails
);
324 public void unregister() throws IOException
{
325 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
326 // If this is the master device, other users can't send messages to this number anymore.
327 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
328 accountManager
.setGcmId(Optional
.absent());
330 account
.setRegistered(false);
334 public List
<DeviceInfo
> getLinkedDevices() throws IOException
{
335 List
<DeviceInfo
> devices
= accountManager
.getDevices();
336 account
.setMultiDevice(devices
.size() > 1);
341 public void removeLinkedDevices(int deviceId
) throws IOException
{
342 accountManager
.removeDevice(deviceId
);
343 List
<DeviceInfo
> devices
= accountManager
.getDevices();
344 account
.setMultiDevice(devices
.size() > 1);
348 public void addDeviceLink(URI linkUri
) throws IOException
, InvalidKeyException
{
349 Utils
.DeviceLinkInfo info
= Utils
.parseDeviceLinkUri(linkUri
);
351 addDevice(info
.deviceIdentifier
, info
.deviceKey
);
354 private void addDevice(String deviceIdentifier
, ECPublicKey deviceKey
) throws IOException
, InvalidKeyException
{
355 IdentityKeyPair identityKeyPair
= getIdentityKeyPair();
356 String verificationCode
= accountManager
.getNewDeviceVerificationCode();
358 accountManager
.addDevice(deviceIdentifier
, deviceKey
, identityKeyPair
, Optional
.of(account
.getProfileKey().serialize()), verificationCode
);
359 account
.setMultiDevice(true);
363 private List
<PreKeyRecord
> generatePreKeys() {
364 List
<PreKeyRecord
> records
= new ArrayList
<>(ServiceConfig
.PREKEY_BATCH_SIZE
);
366 final int offset
= account
.getPreKeyIdOffset();
367 for (int i
= 0; i
< ServiceConfig
.PREKEY_BATCH_SIZE
; i
++) {
368 int preKeyId
= (offset
+ i
) % Medium
.MAX_VALUE
;
369 ECKeyPair keyPair
= Curve
.generateKeyPair();
370 PreKeyRecord
record = new PreKeyRecord(preKeyId
, keyPair
);
375 account
.addPreKeys(records
);
381 private SignedPreKeyRecord
generateSignedPreKey(IdentityKeyPair identityKeyPair
) {
383 ECKeyPair keyPair
= Curve
.generateKeyPair();
384 byte[] signature
= Curve
.calculateSignature(identityKeyPair
.getPrivateKey(), keyPair
.getPublicKey().serialize());
385 SignedPreKeyRecord
record = new SignedPreKeyRecord(account
.getNextSignedPreKeyId(), System
.currentTimeMillis(), keyPair
, signature
);
387 account
.addSignedPreKey(record);
391 } catch (InvalidKeyException e
) {
392 throw new AssertionError(e
);
396 public void verifyAccount(String verificationCode
, String pin
) throws IOException
{
397 verificationCode
= verificationCode
.replace("-", "");
398 account
.setSignalingKey(KeyUtils
.createSignalingKey());
399 // TODO make unrestricted unidentified access configurable
400 VerifyAccountResponse response
= accountManager
.verifyAccountWithCode(verificationCode
, account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, pin
, null, getSelfUnidentifiedAccessKey(), false, capabilities
, discoverableByPhoneNumber
);
402 UUID uuid
= UuidUtil
.parseOrNull(response
.getUuid());
403 // TODO response.isStorageCapable()
404 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
405 account
.setRegistered(true);
406 account
.setUuid(uuid
);
407 account
.setRegistrationLockPin(pin
);
408 account
.getSignalProtocolStore().saveIdentity(account
.getSelfAddress(), getIdentityKeyPair().getPublicKey(), TrustLevel
.TRUSTED_VERIFIED
);
414 public void setRegistrationLockPin(Optional
<String
> pin
) throws IOException
{
415 if (pin
.isPresent()) {
416 account
.setRegistrationLockPin(pin
.get());
417 throw new RuntimeException("Not implemented anymore, will be replaced with KBS");
419 account
.setRegistrationLockPin(null);
420 accountManager
.removeRegistrationLockV1();
425 void refreshPreKeys() throws IOException
{
426 List
<PreKeyRecord
> oneTimePreKeys
= generatePreKeys();
427 final IdentityKeyPair identityKeyPair
= getIdentityKeyPair();
428 SignedPreKeyRecord signedPreKeyRecord
= generateSignedPreKey(identityKeyPair
);
430 accountManager
.setPreKeys(identityKeyPair
.getPublicKey(), signedPreKeyRecord
, oneTimePreKeys
);
433 private SignalServiceMessageReceiver
getMessageReceiver() {
434 final ClientZkProfileOperations clientZkProfileOperations
= capabilities
.isGv2()
435 ? ClientZkOperations
.create(serviceConfiguration
).getProfileOperations()
437 return new SignalServiceMessageReceiver(serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), account
.getSignalingKey(), userAgent
, null, timer
, clientZkProfileOperations
);
440 private SignalServiceMessageSender
getMessageSender() {
441 final ClientZkProfileOperations clientZkProfileOperations
= capabilities
.isGv2()
442 ? ClientZkOperations
.create(serviceConfiguration
).getProfileOperations()
444 final ExecutorService executor
= null;
445 return new SignalServiceMessageSender(serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(),
446 account
.getDeviceId(), account
.getSignalProtocolStore(), userAgent
, account
.isMultiDevice(), Optional
.fromNullable(messagePipe
), Optional
.fromNullable(unidentifiedMessagePipe
), Optional
.absent(), clientZkProfileOperations
, executor
, ServiceConfig
.MAX_ENVELOPE_SIZE
);
449 private SignalServiceProfile
getEncryptedRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
) throws IOException
{
450 SignalServiceMessagePipe pipe
= unidentifiedMessagePipe
!= null && unidentifiedAccess
.isPresent() ? unidentifiedMessagePipe
455 return pipe
.getProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).get(10, TimeUnit
.SECONDS
).getProfile();
456 } catch (IOException
| InterruptedException
| ExecutionException
| TimeoutException ignored
) {
460 SignalServiceMessageReceiver receiver
= getMessageReceiver();
462 return receiver
.retrieveProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).get(10, TimeUnit
.SECONDS
).getProfile();
463 } catch (InterruptedException
| ExecutionException
| TimeoutException e
) {
464 throw new IOException("Failed to retrieve profile", e
);
468 private SignalProfile
getRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
, ProfileKey profileKey
) throws IOException
{
469 SignalProfileEntry profileEntry
= account
.getProfileStore().getProfile(address
);
470 long now
= new Date().getTime();
471 // Profiles are cache for 24h before retrieving them again
472 if (profileEntry
== null || profileEntry
.getProfile() == null || now
- profileEntry
.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000) {
473 SignalProfile profile
= retrieveRecipientProfile(address
, unidentifiedAccess
, profileKey
);
474 account
.getProfileStore().updateProfile(address
, profileKey
, now
, profile
);
477 return profileEntry
.getProfile();
480 private SignalProfile
retrieveRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
, ProfileKey profileKey
) throws IOException
{
481 final SignalServiceProfile encryptedProfile
= getEncryptedRecipientProfile(address
, unidentifiedAccess
);
483 File avatarFile
= null;
485 avatarFile
= encryptedProfile
.getAvatar() == null ?
null : retrieveProfileAvatar(address
, encryptedProfile
.getAvatar(), profileKey
);
486 } catch (Throwable e
) {
487 System
.err
.println("Failed to retrieve profile avatar, ignoring: " + e
.getMessage());
490 ProfileCipher profileCipher
= new ProfileCipher(profileKey
);
492 return new SignalProfile(
493 encryptedProfile
.getIdentityKey(),
494 encryptedProfile
.getName() == null ?
null : new String(profileCipher
.decryptName(Base64
.decode(encryptedProfile
.getName()))),
496 encryptedProfile
.getUnidentifiedAccess() == null || !profileCipher
.verifyUnidentifiedAccess(Base64
.decode(encryptedProfile
.getUnidentifiedAccess())) ?
null : encryptedProfile
.getUnidentifiedAccess(),
497 encryptedProfile
.isUnrestrictedUnidentifiedAccess(),
498 encryptedProfile
.getCapabilities());
499 } catch (InvalidCiphertextException e
) {
504 private Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(byte[] groupId
) throws IOException
{
505 File file
= getGroupAvatarFile(groupId
);
506 if (!file
.exists()) {
507 return Optional
.absent();
510 return Optional
.of(Utils
.createAttachment(file
));
513 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(String number
) throws IOException
{
514 File file
= getContactAvatarFile(number
);
515 if (!file
.exists()) {
516 return Optional
.absent();
519 return Optional
.of(Utils
.createAttachment(file
));
522 private GroupInfo
getGroupForSending(byte[] groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
523 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
525 throw new GroupNotFoundException(groupId
);
527 if (!g
.isMember(account
.getSelfAddress())) {
528 throw new NotAGroupMemberException(groupId
, g
.getTitle());
533 public List
<GroupInfo
> getGroups() {
534 return account
.getGroupStore().getGroups();
537 public Pair
<Long
, List
<SendMessageResult
>> sendGroupMessage(
539 List
<String
> attachments
,
542 throws IOException
, GroupNotFoundException
, AttachmentInvalidException
, NotAGroupMemberException
{
543 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
544 if (attachments
!= null) {
545 messageBuilder
.withAttachments(Utils
.getSignalServiceAttachments(attachments
));
548 final GroupInfo g
= getGroupForSending(groupId
);
550 setGroupContext(messageBuilder
, g
);
551 messageBuilder
.withExpiration(g
.getMessageExpirationTime());
553 return sendMessage(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
556 private void setGroupContext(final SignalServiceDataMessage
.Builder messageBuilder
, final GroupInfo groupInfo
) {
557 if (groupInfo
instanceof GroupInfoV1
) {
558 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
559 .withId(groupInfo
.groupId
)
561 messageBuilder
.asGroupMessage(group
);
563 final GroupInfoV2 groupInfoV2
= (GroupInfoV2
) groupInfo
;
564 SignalServiceGroupV2 group
= SignalServiceGroupV2
.newBuilder(groupInfoV2
.getMasterKey())
565 .withRevision(groupInfoV2
.getGroup() == null ?
0 : groupInfoV2
.getGroup().getRevision())
567 messageBuilder
.asGroupMessage(group
);
571 public Pair
<Long
, List
<SendMessageResult
>> sendGroupMessageReaction(String emoji
, boolean remove
, String targetAuthor
,
572 long targetSentTimestamp
, byte[] groupId
)
573 throws IOException
, InvalidNumberException
, NotAGroupMemberException
, GroupNotFoundException
{
574 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, canonicalizeAndResolveSignalServiceAddress(targetAuthor
), targetSentTimestamp
);
575 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
576 .withReaction(reaction
);
577 final GroupInfo g
= getGroupForSending(groupId
);
578 setGroupContext(messageBuilder
, g
);
579 return sendMessage(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
582 public Pair
<Long
, List
<SendMessageResult
>> sendQuitGroupMessage(byte[] groupId
) throws GroupNotFoundException
, IOException
, NotAGroupMemberException
{
583 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
587 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
588 .asGroupMessage(group
);
590 final GroupInfo g
= getGroupForSending(groupId
);
591 if (g
instanceof GroupInfoV1
) {
592 GroupInfoV1 groupInfoV1
= (GroupInfoV1
) g
;
593 groupInfoV1
.removeMember(account
.getSelfAddress());
594 account
.getGroupStore().updateGroup(groupInfoV1
);
596 throw new RuntimeException("TODO Not implemented!");
599 return sendMessage(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
602 private Pair
<byte[], List
<SendMessageResult
>> sendUpdateGroupMessage(byte[] groupId
, String name
, Collection
<SignalServiceAddress
> members
, String avatarFile
) throws IOException
, GroupNotFoundException
, AttachmentInvalidException
, NotAGroupMemberException
{
604 if (groupId
== null) {
606 g
= new GroupInfoV1(KeyUtils
.createGroupId());
607 g
.addMembers(Collections
.singleton(account
.getSelfAddress()));
609 GroupInfo group
= getGroupForSending(groupId
);
610 if (!(group
instanceof GroupInfoV1
)) {
611 throw new RuntimeException("TODO Not implemented!");
613 g
= (GroupInfoV1
) group
;
620 if (members
!= null) {
621 final Set
<String
> newE164Members
= new HashSet
<>();
622 for (SignalServiceAddress member
: members
) {
623 if (g
.isMember(member
) || !member
.getNumber().isPresent()) {
626 newE164Members
.add(member
.getNumber().get());
629 final List
<ContactTokenDetails
> contacts
= accountManager
.getContacts(newE164Members
);
630 if (contacts
.size() != newE164Members
.size()) {
631 // Some of the new members are not registered on Signal
632 for (ContactTokenDetails contact
: contacts
) {
633 newE164Members
.remove(contact
.getNumber());
635 throw new IOException("Failed to add members " + Util
.join(", ", newE164Members
) + " to group: Not registered on Signal");
638 g
.addMembers(members
);
641 if (avatarFile
!= null) {
642 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
643 File aFile
= getGroupAvatarFile(g
.groupId
);
644 Files
.copy(Paths
.get(avatarFile
), aFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
647 account
.getGroupStore().updateGroup(g
);
649 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
651 final Pair
<Long
, List
<SendMessageResult
>> result
= sendMessage(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
652 return new Pair
<>(g
.groupId
, result
.second());
655 Pair
<Long
, List
<SendMessageResult
>> sendUpdateGroupMessage(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
, AttachmentInvalidException
{
657 GroupInfo group
= getGroupForSending(groupId
);
658 if (!(group
instanceof GroupInfoV1
)) {
659 throw new RuntimeException("TODO Not implemented!");
661 g
= (GroupInfoV1
) group
;
663 if (!g
.isMember(recipient
)) {
664 throw new NotAGroupMemberException(groupId
, g
.name
);
667 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
669 // Send group message only to the recipient who requested it
670 return sendMessage(messageBuilder
, Collections
.singleton(recipient
));
673 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfoV1 g
) throws AttachmentInvalidException
{
674 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
677 .withMembers(new ArrayList
<>(g
.getMembers()));
679 File aFile
= getGroupAvatarFile(g
.groupId
);
680 if (aFile
.exists()) {
682 group
.withAvatar(Utils
.createAttachment(aFile
));
683 } catch (IOException e
) {
684 throw new AttachmentInvalidException(aFile
.toString(), e
);
688 return SignalServiceDataMessage
.newBuilder()
689 .asGroupMessage(group
.build())
690 .withExpiration(g
.messageExpirationTime
);
693 Pair
<Long
, List
<SendMessageResult
>> sendGroupInfoRequest(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
{
694 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
)
697 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
698 .asGroupMessage(group
.build());
700 // Send group info request message to the recipient who sent us a message with this groupId
701 return sendMessage(messageBuilder
, Collections
.singleton(recipient
));
704 void sendReceipt(SignalServiceAddress remoteAddress
, long messageId
) throws IOException
, UntrustedIdentityException
{
705 SignalServiceReceiptMessage receiptMessage
= new SignalServiceReceiptMessage(SignalServiceReceiptMessage
.Type
.DELIVERY
,
706 Collections
.singletonList(messageId
),
707 System
.currentTimeMillis());
709 getMessageSender().sendReceipt(remoteAddress
, getAccessFor(remoteAddress
), receiptMessage
);
712 public Pair
<Long
, List
<SendMessageResult
>> sendMessage(String messageText
, List
<String
> attachments
,
713 List
<String
> recipients
)
714 throws IOException
, AttachmentInvalidException
, InvalidNumberException
{
715 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
716 if (attachments
!= null) {
717 List
<SignalServiceAttachment
> attachmentStreams
= Utils
.getSignalServiceAttachments(attachments
);
719 // Upload attachments here, so we only upload once even for multiple recipients
720 SignalServiceMessageSender messageSender
= getMessageSender();
721 List
<SignalServiceAttachment
> attachmentPointers
= new ArrayList
<>(attachmentStreams
.size());
722 for (SignalServiceAttachment attachment
: attachmentStreams
) {
723 if (attachment
.isStream()) {
724 attachmentPointers
.add(messageSender
.uploadAttachment(attachment
.asStream()));
725 } else if (attachment
.isPointer()) {
726 attachmentPointers
.add(attachment
.asPointer());
730 messageBuilder
.withAttachments(attachmentPointers
);
732 return sendMessage(messageBuilder
, getSignalServiceAddresses(recipients
));
735 public Pair
<Long
, List
<SendMessageResult
>> sendMessageReaction(String emoji
, boolean remove
, String targetAuthor
,
736 long targetSentTimestamp
, List
<String
> recipients
)
737 throws IOException
, InvalidNumberException
{
738 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, canonicalizeAndResolveSignalServiceAddress(targetAuthor
), targetSentTimestamp
);
739 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
740 .withReaction(reaction
);
741 return sendMessage(messageBuilder
, getSignalServiceAddresses(recipients
));
744 public Pair
<Long
, List
<SendMessageResult
>> sendEndSessionMessage(List
<String
> recipients
) throws IOException
, InvalidNumberException
{
745 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
746 .asEndSessionMessage();
748 final Collection
<SignalServiceAddress
> signalServiceAddresses
= getSignalServiceAddresses(recipients
);
750 return sendMessage(messageBuilder
, signalServiceAddresses
);
751 } catch (Exception e
) {
752 for (SignalServiceAddress address
: signalServiceAddresses
) {
753 handleEndSession(address
);
760 public String
getContactName(String number
) throws InvalidNumberException
{
761 ContactInfo contact
= account
.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number
));
762 if (contact
== null) {
769 public void setContactName(String number
, String name
) throws InvalidNumberException
{
770 final SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(number
);
771 ContactInfo contact
= account
.getContactStore().getContact(address
);
772 if (contact
== null) {
773 contact
= new ContactInfo(address
);
776 account
.getContactStore().updateContact(contact
);
780 public void setContactBlocked(String number
, boolean blocked
) throws InvalidNumberException
{
781 setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number
), blocked
);
784 private void setContactBlocked(SignalServiceAddress address
, boolean blocked
) {
785 ContactInfo contact
= account
.getContactStore().getContact(address
);
786 if (contact
== null) {
787 contact
= new ContactInfo(address
);
789 contact
.blocked
= blocked
;
790 account
.getContactStore().updateContact(contact
);
794 public void setGroupBlocked(final byte[] groupId
, final boolean blocked
) throws GroupNotFoundException
{
795 GroupInfo group
= getGroup(groupId
);
797 throw new GroupNotFoundException(groupId
);
800 group
.setBlocked(blocked
);
801 account
.getGroupStore().updateGroup(group
);
805 public Pair
<byte[], List
<SendMessageResult
>> updateGroup(byte[] groupId
, String name
, List
<String
> members
, String avatar
) throws IOException
, GroupNotFoundException
, AttachmentInvalidException
, InvalidNumberException
, NotAGroupMemberException
{
806 if (groupId
.length
== 0) {
809 if (name
.isEmpty()) {
812 if (members
.isEmpty()) {
815 if (avatar
.isEmpty()) {
818 return sendUpdateGroupMessage(groupId
, name
, members
== null ?
null : getSignalServiceAddresses(members
), avatar
);
822 * Change the expiration timer for a contact
824 public void setExpirationTimer(SignalServiceAddress address
, int messageExpirationTimer
) throws IOException
{
825 ContactInfo contact
= account
.getContactStore().getContact(address
);
826 contact
.messageExpirationTime
= messageExpirationTimer
;
827 account
.getContactStore().updateContact(contact
);
828 sendExpirationTimerUpdate(address
);
832 private void sendExpirationTimerUpdate(SignalServiceAddress address
) throws IOException
{
833 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
834 .asExpirationUpdate();
835 sendMessage(messageBuilder
, Collections
.singleton(address
));
839 * Change the expiration timer for a contact
841 public void setExpirationTimer(String number
, int messageExpirationTimer
) throws IOException
, InvalidNumberException
{
842 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(number
);
843 setExpirationTimer(address
, messageExpirationTimer
);
847 * Change the expiration timer for a group
849 public void setExpirationTimer(byte[] groupId
, int messageExpirationTimer
) {
850 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
851 if (g
instanceof GroupInfoV1
) {
852 GroupInfoV1 groupInfoV1
= (GroupInfoV1
) g
;
853 groupInfoV1
.messageExpirationTime
= messageExpirationTimer
;
854 account
.getGroupStore().updateGroup(groupInfoV1
);
856 throw new RuntimeException("TODO Not implemented!");
861 * Upload the sticker pack from path.
863 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
864 * @return if successful, returns the URL to install the sticker pack in the signal app
866 public String
uploadStickerPack(String path
) throws IOException
, StickerPackInvalidException
{
867 SignalServiceStickerManifestUpload manifest
= getSignalServiceStickerManifestUpload(path
);
869 SignalServiceMessageSender messageSender
= getMessageSender();
871 byte[] packKey
= KeyUtils
.createStickerUploadKey();
872 String packId
= messageSender
.uploadStickerManifest(manifest
, packKey
);
875 return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder
.encode(packId
, StandardCharsets
.UTF_8
) + "&pack_key=" + URLEncoder
.encode(Hex
.toStringCondensed(packKey
), StandardCharsets
.UTF_8
))
877 } catch (URISyntaxException e
) {
878 throw new AssertionError(e
);
882 private SignalServiceStickerManifestUpload
getSignalServiceStickerManifestUpload(final String path
) throws IOException
, StickerPackInvalidException
{
884 String rootPath
= null;
886 final File file
= new File(path
);
887 if (file
.getName().endsWith(".zip")) {
888 zip
= new ZipFile(file
);
889 } else if (file
.getName().equals("manifest.json")) {
890 rootPath
= file
.getParent();
892 throw new StickerPackInvalidException("Could not find manifest.json");
895 JsonStickerPack pack
= parseStickerPack(rootPath
, zip
);
897 if (pack
.stickers
== null) {
898 throw new StickerPackInvalidException("Must set a 'stickers' field.");
901 if (pack
.stickers
.isEmpty()) {
902 throw new StickerPackInvalidException("Must include stickers.");
905 List
<StickerInfo
> stickers
= new ArrayList
<>(pack
.stickers
.size());
906 for (JsonStickerPack
.JsonSticker sticker
: pack
.stickers
) {
907 if (sticker
.file
== null) {
908 throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
911 Pair
<InputStream
, Long
> data
;
913 data
= getInputStreamAndLength(rootPath
, zip
, sticker
.file
);
914 } catch (IOException ignored
) {
915 throw new StickerPackInvalidException("Could not find find " + sticker
.file
);
918 String contentType
= Utils
.getFileMimeType(new File(sticker
.file
), null);
919 StickerInfo stickerInfo
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(sticker
.emoji
).or(""), contentType
);
920 stickers
.add(stickerInfo
);
923 StickerInfo cover
= null;
924 if (pack
.cover
!= null) {
925 if (pack
.cover
.file
== null) {
926 throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
929 Pair
<InputStream
, Long
> data
;
931 data
= getInputStreamAndLength(rootPath
, zip
, pack
.cover
.file
);
932 } catch (IOException ignored
) {
933 throw new StickerPackInvalidException("Could not find find " + pack
.cover
.file
);
936 String contentType
= Utils
.getFileMimeType(new File(pack
.cover
.file
), null);
937 cover
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(pack
.cover
.emoji
).or(""), contentType
);
940 return new SignalServiceStickerManifestUpload(
947 private static JsonStickerPack
parseStickerPack(String rootPath
, ZipFile zip
) throws IOException
{
948 InputStream inputStream
;
950 inputStream
= zip
.getInputStream(zip
.getEntry("manifest.json"));
952 inputStream
= new FileInputStream((new File(rootPath
, "manifest.json")));
954 return new ObjectMapper().readValue(inputStream
, JsonStickerPack
.class);
957 private static Pair
<InputStream
, Long
> getInputStreamAndLength(final String rootPath
, final ZipFile zip
, final String subfile
) throws IOException
{
959 final ZipEntry entry
= zip
.getEntry(subfile
);
960 return new Pair
<>(zip
.getInputStream(entry
), entry
.getSize());
962 final File file
= new File(rootPath
, subfile
);
963 return new Pair
<>(new FileInputStream(file
), file
.length());
967 void requestSyncGroups() throws IOException
{
968 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
).build();
969 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
971 sendSyncMessage(message
);
972 } catch (UntrustedIdentityException e
) {
977 void requestSyncContacts() throws IOException
{
978 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
).build();
979 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
981 sendSyncMessage(message
);
982 } catch (UntrustedIdentityException e
) {
987 void requestSyncBlocked() throws IOException
{
988 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
).build();
989 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
991 sendSyncMessage(message
);
992 } catch (UntrustedIdentityException e
) {
997 void requestSyncConfiguration() throws IOException
{
998 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
).build();
999 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1001 sendSyncMessage(message
);
1002 } catch (UntrustedIdentityException e
) {
1003 e
.printStackTrace();
1007 private byte[] getSenderCertificate() {
1008 // TODO support UUID capable sender certificates
1009 // byte[] certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy();
1012 certificate
= accountManager
.getSenderCertificate();
1013 } catch (IOException e
) {
1014 System
.err
.println("Failed to get sender certificate: " + e
);
1017 // TODO cache for a day
1021 private byte[] getSelfUnidentifiedAccessKey() {
1022 return UnidentifiedAccess
.deriveAccessKeyFrom(account
.getProfileKey());
1025 private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient
) {
1026 ProfileKey theirProfileKey
= account
.getProfileStore().getProfileKey(recipient
);
1027 if (theirProfileKey
== null) {
1030 SignalProfile targetProfile
;
1032 targetProfile
= getRecipientProfile(recipient
, Optional
.absent(), theirProfileKey
);
1033 } catch (IOException e
) {
1034 System
.err
.println("Failed to get recipient profile: " + e
);
1038 if (targetProfile
== null || targetProfile
.getUnidentifiedAccess() == null) {
1042 if (targetProfile
.isUnrestrictedUnidentifiedAccess()) {
1043 return KeyUtils
.createUnrestrictedUnidentifiedAccess();
1046 return UnidentifiedAccess
.deriveAccessKeyFrom(theirProfileKey
);
1049 private Optional
<UnidentifiedAccessPair
> getAccessForSync() {
1050 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1051 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1053 if (selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1054 return Optional
.absent();
1058 return Optional
.of(new UnidentifiedAccessPair(
1059 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1060 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1062 } catch (InvalidCertificateException e
) {
1063 return Optional
.absent();
1067 private List
<Optional
<UnidentifiedAccessPair
>> getAccessFor(Collection
<SignalServiceAddress
> recipients
) {
1068 List
<Optional
<UnidentifiedAccessPair
>> result
= new ArrayList
<>(recipients
.size());
1069 for (SignalServiceAddress recipient
: recipients
) {
1070 result
.add(getAccessFor(recipient
));
1075 private Optional
<UnidentifiedAccessPair
> getAccessFor(SignalServiceAddress recipient
) {
1076 byte[] recipientUnidentifiedAccessKey
= getTargetUnidentifiedAccessKey(recipient
);
1077 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1078 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1080 if (recipientUnidentifiedAccessKey
== null || selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1081 return Optional
.absent();
1085 return Optional
.of(new UnidentifiedAccessPair(
1086 new UnidentifiedAccess(recipientUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1087 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1089 } catch (InvalidCertificateException e
) {
1090 return Optional
.absent();
1094 private Optional
<UnidentifiedAccess
> getUnidentifiedAccess(SignalServiceAddress recipient
) {
1095 Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1097 if (unidentifiedAccess
.isPresent()) {
1098 return unidentifiedAccess
.get().getTargetUnidentifiedAccess();
1101 return Optional
.absent();
1104 private void sendSyncMessage(SignalServiceSyncMessage message
)
1105 throws IOException
, UntrustedIdentityException
{
1106 SignalServiceMessageSender messageSender
= getMessageSender();
1108 messageSender
.sendMessage(message
, getAccessForSync());
1109 } catch (UntrustedIdentityException e
) {
1110 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1115 private Collection
<SignalServiceAddress
> getSignalServiceAddresses(Collection
<String
> numbers
) throws InvalidNumberException
{
1116 final Set
<SignalServiceAddress
> signalServiceAddresses
= new HashSet
<>(numbers
.size());
1118 for (String number
: numbers
) {
1119 signalServiceAddresses
.add(canonicalizeAndResolveSignalServiceAddress(number
));
1121 return signalServiceAddresses
;
1124 private Pair
<Long
, List
<SendMessageResult
>> sendMessage(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1125 throws IOException
{
1126 recipients
= recipients
.stream().map(this::resolveSignalServiceAddress
).collect(Collectors
.toSet());
1127 final long timestamp
= System
.currentTimeMillis();
1128 messageBuilder
.withTimestamp(timestamp
);
1129 if (messagePipe
== null) {
1130 messagePipe
= getMessageReceiver().createMessagePipe();
1132 if (unidentifiedMessagePipe
== null) {
1133 unidentifiedMessagePipe
= getMessageReceiver().createUnidentifiedMessagePipe();
1135 SignalServiceDataMessage message
= null;
1137 message
= messageBuilder
.build();
1138 if (message
.getGroupContext().isPresent()) {
1140 SignalServiceMessageSender messageSender
= getMessageSender();
1141 final boolean isRecipientUpdate
= false;
1142 List
<SendMessageResult
> result
= messageSender
.sendMessage(new ArrayList
<>(recipients
), getAccessFor(recipients
), isRecipientUpdate
, message
);
1143 for (SendMessageResult r
: result
) {
1144 if (r
.getIdentityFailure() != null) {
1145 account
.getSignalProtocolStore().saveIdentity(r
.getAddress(), r
.getIdentityFailure().getIdentityKey(), TrustLevel
.UNTRUSTED
);
1148 return new Pair
<>(timestamp
, result
);
1149 } catch (UntrustedIdentityException e
) {
1150 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1151 return new Pair
<>(timestamp
, Collections
.emptyList());
1154 // Send to all individually, so sync messages are sent correctly
1155 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1156 for (SignalServiceAddress address
: recipients
) {
1157 ContactInfo contact
= account
.getContactStore().getContact(address
);
1158 if (contact
!= null) {
1159 messageBuilder
.withExpiration(contact
.messageExpirationTime
);
1160 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
1162 messageBuilder
.withExpiration(0);
1163 messageBuilder
.withProfileKey(null);
1165 message
= messageBuilder
.build();
1166 if (address
.matches(account
.getSelfAddress())) {
1167 results
.add(sendSelfMessage(message
));
1169 results
.add(sendMessage(address
, message
));
1172 return new Pair
<>(timestamp
, results
);
1175 if (message
!= null && message
.isEndSession()) {
1176 for (SignalServiceAddress recipient
: recipients
) {
1177 handleEndSession(recipient
);
1184 private SendMessageResult
sendSelfMessage(SignalServiceDataMessage message
) throws IOException
{
1185 SignalServiceMessageSender messageSender
= getMessageSender();
1187 SignalServiceAddress recipient
= account
.getSelfAddress();
1189 final Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1190 SentTranscriptMessage transcript
= new SentTranscriptMessage(Optional
.of(recipient
),
1191 message
.getTimestamp(),
1193 message
.getExpiresInSeconds(),
1194 Collections
.singletonMap(recipient
, unidentifiedAccess
.isPresent()),
1196 SignalServiceSyncMessage syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
1199 long startTime
= System
.currentTimeMillis();
1200 messageSender
.sendMessage(syncMessage
, unidentifiedAccess
);
1201 return SendMessageResult
.success(recipient
, unidentifiedAccess
.isPresent(), false, System
.currentTimeMillis() - startTime
);
1202 } catch (UntrustedIdentityException e
) {
1203 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1204 return SendMessageResult
.identityFailure(recipient
, e
.getIdentityKey());
1208 private SendMessageResult
sendMessage(SignalServiceAddress address
, SignalServiceDataMessage message
) throws IOException
{
1209 SignalServiceMessageSender messageSender
= getMessageSender();
1212 return messageSender
.sendMessage(address
, getAccessFor(address
), message
);
1213 } catch (UntrustedIdentityException e
) {
1214 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1215 return SendMessageResult
.identityFailure(address
, e
.getIdentityKey());
1219 private SignalServiceContent
decryptMessage(SignalServiceEnvelope envelope
) throws InvalidMetadataMessageException
, ProtocolInvalidMessageException
, ProtocolDuplicateMessageException
, ProtocolLegacyMessageException
, ProtocolInvalidKeyIdException
, InvalidMetadataVersionException
, ProtocolInvalidVersionException
, ProtocolNoSessionException
, ProtocolInvalidKeyException
, SelfSendException
, UnsupportedDataMessageException
, org
.whispersystems
.libsignal
.UntrustedIdentityException
{
1220 SignalServiceCipher cipher
= new SignalServiceCipher(account
.getSelfAddress(), account
.getSignalProtocolStore(), Utils
.getCertificateValidator());
1222 return cipher
.decrypt(envelope
);
1223 } catch (ProtocolUntrustedIdentityException e
) {
1224 if (e
.getCause() instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
) {
1225 org
.whispersystems
.libsignal
.UntrustedIdentityException identityException
= (org
.whispersystems
.libsignal
.UntrustedIdentityException
) e
.getCause();
1226 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(identityException
.getName()), identityException
.getUntrustedIdentity(), TrustLevel
.UNTRUSTED
);
1227 throw identityException
;
1229 throw new AssertionError(e
);
1233 private void handleEndSession(SignalServiceAddress source
) {
1234 account
.getSignalProtocolStore().deleteAllSessions(source
);
1237 private static int currentTimeDays() {
1238 return (int) TimeUnit
.MILLISECONDS
.toDays(System
.currentTimeMillis());
1241 private GroupsV2AuthorizationString
getGroupAuthForToday(final GroupSecretParams groupSecretParams
) throws IOException
, VerificationFailedException
{
1242 final int today
= currentTimeDays();
1243 // Returns credentials for the next 7 days
1244 final HashMap
<Integer
, AuthCredentialResponse
> credentials
= groupsV2Api
.getCredentials(today
);
1245 // TODO cache credentials until they expire
1246 AuthCredentialResponse authCredentialResponse
= credentials
.get(today
);
1247 return groupsV2Api
.getGroupsV2AuthorizationString(account
.getUuid(), today
, groupSecretParams
, authCredentialResponse
);
1250 private List
<HandleAction
> handleSignalServiceDataMessage(SignalServiceDataMessage message
, boolean isSync
, SignalServiceAddress source
, SignalServiceAddress destination
, boolean ignoreAttachments
) {
1251 List
<HandleAction
> actions
= new ArrayList
<>();
1252 if (message
.getGroupContext().isPresent()) {
1253 if (message
.getGroupContext().get().getGroupV1().isPresent()) {
1254 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1255 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1256 if (group
== null || group
instanceof GroupInfoV1
) {
1257 GroupInfoV1 groupV1
= (GroupInfoV1
) group
;
1258 switch (groupInfo
.getType()) {
1260 if (groupV1
== null) {
1261 groupV1
= new GroupInfoV1(groupInfo
.getGroupId());
1264 if (groupInfo
.getAvatar().isPresent()) {
1265 SignalServiceAttachment avatar
= groupInfo
.getAvatar().get();
1266 if (avatar
.isPointer()) {
1268 retrieveGroupAvatarAttachment(avatar
.asPointer(), groupV1
.groupId
);
1269 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1270 System
.err
.println("Failed to retrieve group avatar (" + avatar
.asPointer().getRemoteId() + "): " + e
.getMessage());
1275 if (groupInfo
.getName().isPresent()) {
1276 groupV1
.name
= groupInfo
.getName().get();
1279 if (groupInfo
.getMembers().isPresent()) {
1280 groupV1
.addMembers(groupInfo
.getMembers().get()
1282 .map(this::resolveSignalServiceAddress
)
1283 .collect(Collectors
.toSet()));
1286 account
.getGroupStore().updateGroup(groupV1
);
1290 if (groupV1
== null && !isSync
) {
1291 actions
.add(new SendGroupInfoRequestAction(source
, groupInfo
.getGroupId()));
1295 if (groupV1
!= null) {
1296 groupV1
.removeMember(source
);
1297 account
.getGroupStore().updateGroup(groupV1
);
1302 if (groupV1
!= null && !isSync
) {
1303 actions
.add(new SendGroupUpdateAction(source
, groupV1
.groupId
));
1308 System
.err
.println("Received a group v1 message for a v2 group: " + group
.getTitle());
1311 if (message
.getGroupContext().get().getGroupV2().isPresent()) {
1312 final SignalServiceGroupV2 groupContext
= message
.getGroupContext().get().getGroupV2().get();
1313 final GroupMasterKey groupMasterKey
= groupContext
.getMasterKey();
1315 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
1317 byte[] groupId
= groupSecretParams
.getPublicParams().getGroupIdentifier().serialize();
1318 GroupInfo groupInfo
= account
.getGroupStore().getGroup(groupId
);
1319 if (groupInfo
instanceof GroupInfoV1
) {
1320 // TODO upgrade group
1321 } else if (groupInfo
== null || groupInfo
instanceof GroupInfoV2
) {
1322 GroupInfoV2 groupInfoV2
= groupInfo
== null
1323 ?
new GroupInfoV2(groupId
, groupMasterKey
)
1324 : (GroupInfoV2
) groupInfo
;
1326 if (groupInfoV2
.getGroup() == null || groupInfoV2
.getGroup().getRevision() < groupContext
.getRevision()) {
1327 // TODO check if revision is only 1 behind and a signedGroupChange is available
1329 final GroupsV2AuthorizationString groupsV2AuthorizationString
= getGroupAuthForToday(groupSecretParams
);
1330 final DecryptedGroup group
= groupsV2Api
.getGroup(groupSecretParams
, groupsV2AuthorizationString
);
1331 groupInfoV2
.setGroup(group
);
1332 for (DecryptedMember member
: group
.getMembersList()) {
1333 final SignalServiceAddress address
= resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil
.parseOrThrow(member
.getUuid().toByteArray()), null));
1335 account
.getProfileStore().storeProfileKey(address
, new ProfileKey(member
.getProfileKey().toByteArray()));
1336 } catch (InvalidInputException ignored
) {
1339 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
1340 System
.err
.println("Failed to retrieve Group V2 info, ignoring ...");
1342 account
.getGroupStore().updateGroup(groupInfoV2
);
1347 final SignalServiceAddress conversationPartnerAddress
= isSync ? destination
: source
;
1348 if (message
.isEndSession()) {
1349 handleEndSession(conversationPartnerAddress
);
1351 if (message
.isExpirationUpdate() || message
.getBody().isPresent()) {
1352 if (message
.getGroupContext().isPresent()) {
1353 if (message
.getGroupContext().get().getGroupV1().isPresent()) {
1354 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1355 GroupInfoV1 group
= account
.getGroupStore().getOrCreateGroupV1(groupInfo
.getGroupId());
1356 if (group
!= null) {
1357 if (group
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1358 group
.messageExpirationTime
= message
.getExpiresInSeconds();
1359 account
.getGroupStore().updateGroup(group
);
1362 } else if (message
.getGroupContext().get().getGroupV2().isPresent()) {
1363 // disappearing message timer already stored in the DecryptedGroup
1366 ContactInfo contact
= account
.getContactStore().getContact(conversationPartnerAddress
);
1367 if (contact
== null) {
1368 contact
= new ContactInfo(conversationPartnerAddress
);
1370 if (contact
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1371 contact
.messageExpirationTime
= message
.getExpiresInSeconds();
1372 account
.getContactStore().updateContact(contact
);
1376 if (message
.getAttachments().isPresent() && !ignoreAttachments
) {
1377 for (SignalServiceAttachment attachment
: message
.getAttachments().get()) {
1378 if (attachment
.isPointer()) {
1380 retrieveAttachment(attachment
.asPointer());
1381 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1382 System
.err
.println("Failed to retrieve attachment (" + attachment
.asPointer().getRemoteId() + "): " + e
.getMessage());
1387 if (message
.getProfileKey().isPresent() && message
.getProfileKey().get().length
== 32) {
1388 final ProfileKey profileKey
;
1390 profileKey
= new ProfileKey(message
.getProfileKey().get());
1391 } catch (InvalidInputException e
) {
1392 throw new AssertionError(e
);
1394 if (source
.matches(account
.getSelfAddress())) {
1395 this.account
.setProfileKey(profileKey
);
1397 this.account
.getProfileStore().storeProfileKey(source
, profileKey
);
1399 if (message
.getPreviews().isPresent()) {
1400 final List
<SignalServiceDataMessage
.Preview
> previews
= message
.getPreviews().get();
1401 for (SignalServiceDataMessage
.Preview preview
: previews
) {
1402 if (preview
.getImage().isPresent() && preview
.getImage().get().isPointer()) {
1403 SignalServiceAttachmentPointer attachment
= preview
.getImage().get().asPointer();
1405 retrieveAttachment(attachment
);
1406 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1407 System
.err
.println("Failed to retrieve attachment (" + attachment
.getRemoteId() + "): " + e
.getMessage());
1415 private void retryFailedReceivedMessages(ReceiveMessageHandler handler
, boolean ignoreAttachments
) {
1416 final File cachePath
= new File(getMessageCachePath());
1417 if (!cachePath
.exists()) {
1420 for (final File dir
: Objects
.requireNonNull(cachePath
.listFiles())) {
1421 if (!dir
.isDirectory()) {
1422 retryFailedReceivedMessage(handler
, ignoreAttachments
, dir
);
1426 for (final File fileEntry
: Objects
.requireNonNull(dir
.listFiles())) {
1427 if (!fileEntry
.isFile()) {
1430 retryFailedReceivedMessage(handler
, ignoreAttachments
, fileEntry
);
1432 // Try to delete directory if empty
1437 private void retryFailedReceivedMessage(final ReceiveMessageHandler handler
, final boolean ignoreAttachments
, final File fileEntry
) {
1438 SignalServiceEnvelope envelope
;
1440 envelope
= Utils
.loadEnvelope(fileEntry
);
1441 if (envelope
== null) {
1444 } catch (IOException e
) {
1445 e
.printStackTrace();
1448 SignalServiceContent content
= null;
1449 if (!envelope
.isReceipt()) {
1451 content
= decryptMessage(envelope
);
1452 } catch (org
.whispersystems
.libsignal
.UntrustedIdentityException e
) {
1454 } catch (Exception er
) {
1455 // All other errors are not recoverable, so delete the cached message
1457 Files
.delete(fileEntry
.toPath());
1458 } catch (IOException e
) {
1459 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1463 List
<HandleAction
> actions
= handleMessage(envelope
, content
, ignoreAttachments
);
1464 for (HandleAction action
: actions
) {
1466 action
.execute(this);
1467 } catch (Throwable e
) {
1468 e
.printStackTrace();
1473 handler
.handleMessage(envelope
, content
, null);
1475 Files
.delete(fileEntry
.toPath());
1476 } catch (IOException e
) {
1477 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1481 public void receiveMessages(long timeout
, TimeUnit unit
, boolean returnOnTimeout
, boolean ignoreAttachments
, ReceiveMessageHandler handler
) throws IOException
{
1482 retryFailedReceivedMessages(handler
, ignoreAttachments
);
1483 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1485 Set
<HandleAction
> queuedActions
= null;
1487 if (messagePipe
== null) {
1488 messagePipe
= messageReceiver
.createMessagePipe();
1491 boolean hasCaughtUpWithOldMessages
= false;
1494 SignalServiceEnvelope envelope
;
1495 SignalServiceContent content
= null;
1496 Exception exception
= null;
1497 final long now
= new Date().getTime();
1499 Optional
<SignalServiceEnvelope
> result
= messagePipe
.readOrEmpty(timeout
, unit
, envelope1
-> {
1500 // store message on disk, before acknowledging receipt to the server
1502 String source
= envelope1
.getSourceE164().isPresent() ? envelope1
.getSourceE164().get() : "";
1503 File cacheFile
= getMessageCacheFile(source
, now
, envelope1
.getTimestamp());
1504 Utils
.storeEnvelope(envelope1
, cacheFile
);
1505 } catch (IOException e
) {
1506 System
.err
.println("Failed to store encrypted message in disk cache, ignoring: " + e
.getMessage());
1509 if (result
.isPresent()) {
1510 envelope
= result
.get();
1512 // Received indicator that server queue is empty
1513 hasCaughtUpWithOldMessages
= true;
1515 if (queuedActions
!= null) {
1516 for (HandleAction action
: queuedActions
) {
1518 action
.execute(this);
1519 } catch (Throwable e
) {
1520 e
.printStackTrace();
1524 queuedActions
.clear();
1525 queuedActions
= null;
1528 // Continue to wait another timeout for new messages
1531 } catch (TimeoutException e
) {
1532 if (returnOnTimeout
)
1535 } catch (InvalidVersionException e
) {
1536 System
.err
.println("Ignoring error: " + e
.getMessage());
1540 if (envelope
.hasSource()) {
1541 // Store uuid if we don't have it already
1542 SignalServiceAddress source
= envelope
.getSourceAddress();
1543 resolveSignalServiceAddress(source
);
1545 if (!envelope
.isReceipt()) {
1547 content
= decryptMessage(envelope
);
1548 } catch (Exception e
) {
1551 List
<HandleAction
> actions
= handleMessage(envelope
, content
, ignoreAttachments
);
1552 if (hasCaughtUpWithOldMessages
) {
1553 for (HandleAction action
: actions
) {
1555 action
.execute(this);
1556 } catch (Throwable e
) {
1557 e
.printStackTrace();
1561 if (queuedActions
== null) {
1562 queuedActions
= new HashSet
<>();
1564 queuedActions
.addAll(actions
);
1568 if (!isMessageBlocked(envelope
, content
)) {
1569 handler
.handleMessage(envelope
, content
, exception
);
1571 if (!(exception
instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
)) {
1572 File cacheFile
= null;
1574 String source
= envelope
.getSourceE164().isPresent() ? envelope
.getSourceE164().get() : "";
1575 cacheFile
= getMessageCacheFile(source
, now
, envelope
.getTimestamp());
1576 Files
.delete(cacheFile
.toPath());
1577 // Try to delete directory if empty
1578 new File(getMessageCachePath()).delete();
1579 } catch (IOException e
) {
1580 System
.err
.println("Failed to delete cached message file “" + cacheFile
+ "”: " + e
.getMessage());
1586 private boolean isMessageBlocked(SignalServiceEnvelope envelope
, SignalServiceContent content
) {
1587 SignalServiceAddress source
;
1588 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1589 source
= envelope
.getSourceAddress();
1590 } else if (content
!= null) {
1591 source
= content
.getSender();
1595 ContactInfo sourceContact
= account
.getContactStore().getContact(source
);
1596 if (sourceContact
!= null && sourceContact
.blocked
) {
1600 if (content
!= null && content
.getDataMessage().isPresent()) {
1601 SignalServiceDataMessage message
= content
.getDataMessage().get();
1602 if (message
.getGroupContext().isPresent() && message
.getGroupContext().get().getGroupV1().isPresent()) {
1603 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1604 GroupInfo group
= getGroup(groupInfo
.getGroupId());
1605 return groupInfo
.getType() == SignalServiceGroup
.Type
.DELIVER
&& group
!= null && group
.isBlocked();
1611 private List
<HandleAction
> handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent content
, boolean ignoreAttachments
) {
1612 List
<HandleAction
> actions
= new ArrayList
<>();
1613 if (content
!= null) {
1614 SignalServiceAddress sender
;
1615 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1616 sender
= envelope
.getSourceAddress();
1618 sender
= content
.getSender();
1620 // Store uuid if we don't have it already
1621 resolveSignalServiceAddress(sender
);
1623 if (content
.getDataMessage().isPresent()) {
1624 SignalServiceDataMessage message
= content
.getDataMessage().get();
1626 if (content
.isNeedsReceipt()) {
1627 actions
.add(new SendReceiptAction(sender
, message
.getTimestamp()));
1630 actions
.addAll(handleSignalServiceDataMessage(message
, false, sender
, account
.getSelfAddress(), ignoreAttachments
));
1632 if (content
.getSyncMessage().isPresent()) {
1633 account
.setMultiDevice(true);
1634 SignalServiceSyncMessage syncMessage
= content
.getSyncMessage().get();
1635 if (syncMessage
.getSent().isPresent()) {
1636 SentTranscriptMessage message
= syncMessage
.getSent().get();
1637 actions
.addAll(handleSignalServiceDataMessage(message
.getMessage(), true, sender
, message
.getDestination().orNull(), ignoreAttachments
));
1639 if (syncMessage
.getRequest().isPresent()) {
1640 RequestMessage rm
= syncMessage
.getRequest().get();
1641 if (rm
.isContactsRequest()) {
1642 actions
.add(SendSyncContactsAction
.create());
1644 if (rm
.isGroupsRequest()) {
1645 actions
.add(SendSyncGroupsAction
.create());
1647 if (rm
.isBlockedListRequest()) {
1648 actions
.add(SendSyncBlockedListAction
.create());
1650 // TODO Handle rm.isConfigurationRequest();
1652 if (syncMessage
.getGroups().isPresent()) {
1653 File tmpFile
= null;
1655 tmpFile
= IOUtils
.createTempFile();
1656 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(syncMessage
.getGroups().get().asPointer(), tmpFile
)) {
1657 DeviceGroupsInputStream s
= new DeviceGroupsInputStream(attachmentAsStream
);
1659 while ((g
= s
.read()) != null) {
1660 GroupInfoV1 syncGroup
= account
.getGroupStore().getOrCreateGroupV1(g
.getId());
1661 if (syncGroup
!= null) {
1662 if (g
.getName().isPresent()) {
1663 syncGroup
.name
= g
.getName().get();
1665 syncGroup
.addMembers(g
.getMembers()
1667 .map(this::resolveSignalServiceAddress
)
1668 .collect(Collectors
.toSet()));
1669 if (!g
.isActive()) {
1670 syncGroup
.removeMember(account
.getSelfAddress());
1672 // Add ourself to the member set as it's marked as active
1673 syncGroup
.addMembers(Collections
.singleton(account
.getSelfAddress()));
1675 syncGroup
.blocked
= g
.isBlocked();
1676 if (g
.getColor().isPresent()) {
1677 syncGroup
.color
= g
.getColor().get();
1680 if (g
.getAvatar().isPresent()) {
1681 retrieveGroupAvatarAttachment(g
.getAvatar().get(), syncGroup
.groupId
);
1683 syncGroup
.inboxPosition
= g
.getInboxPosition().orNull();
1684 syncGroup
.archived
= g
.isArchived();
1685 account
.getGroupStore().updateGroup(syncGroup
);
1689 } catch (Exception e
) {
1690 e
.printStackTrace();
1692 if (tmpFile
!= null) {
1694 Files
.delete(tmpFile
.toPath());
1695 } catch (IOException e
) {
1696 System
.err
.println("Failed to delete received groups temp file “" + tmpFile
+ "”: " + e
.getMessage());
1701 if (syncMessage
.getBlockedList().isPresent()) {
1702 final BlockedListMessage blockedListMessage
= syncMessage
.getBlockedList().get();
1703 for (SignalServiceAddress address
: blockedListMessage
.getAddresses()) {
1704 setContactBlocked(resolveSignalServiceAddress(address
), true);
1706 for (byte[] groupId
: blockedListMessage
.getGroupIds()) {
1708 setGroupBlocked(groupId
, true);
1709 } catch (GroupNotFoundException e
) {
1710 System
.err
.println("BlockedListMessage contained groupID that was not found in GroupStore: " + Base64
.encodeBytes(groupId
));
1714 if (syncMessage
.getContacts().isPresent()) {
1715 File tmpFile
= null;
1717 tmpFile
= IOUtils
.createTempFile();
1718 final ContactsMessage contactsMessage
= syncMessage
.getContacts().get();
1719 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(contactsMessage
.getContactsStream().asPointer(), tmpFile
)) {
1720 DeviceContactsInputStream s
= new DeviceContactsInputStream(attachmentAsStream
);
1721 if (contactsMessage
.isComplete()) {
1722 account
.getContactStore().clear();
1725 while ((c
= s
.read()) != null) {
1726 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
1727 account
.setProfileKey(c
.getProfileKey().get());
1729 final SignalServiceAddress address
= resolveSignalServiceAddress(c
.getAddress());
1730 ContactInfo contact
= account
.getContactStore().getContact(address
);
1731 if (contact
== null) {
1732 contact
= new ContactInfo(address
);
1734 if (c
.getName().isPresent()) {
1735 contact
.name
= c
.getName().get();
1737 if (c
.getColor().isPresent()) {
1738 contact
.color
= c
.getColor().get();
1740 if (c
.getProfileKey().isPresent()) {
1741 account
.getProfileStore().storeProfileKey(address
, c
.getProfileKey().get());
1743 if (c
.getVerified().isPresent()) {
1744 final VerifiedMessage verifiedMessage
= c
.getVerified().get();
1745 account
.getSignalProtocolStore().setIdentityTrustLevel(verifiedMessage
.getDestination(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1747 if (c
.getExpirationTimer().isPresent()) {
1748 contact
.messageExpirationTime
= c
.getExpirationTimer().get();
1750 contact
.blocked
= c
.isBlocked();
1751 contact
.inboxPosition
= c
.getInboxPosition().orNull();
1752 contact
.archived
= c
.isArchived();
1753 account
.getContactStore().updateContact(contact
);
1755 if (c
.getAvatar().isPresent()) {
1756 retrieveContactAvatarAttachment(c
.getAvatar().get(), contact
.number
);
1760 } catch (Exception e
) {
1761 e
.printStackTrace();
1763 if (tmpFile
!= null) {
1765 Files
.delete(tmpFile
.toPath());
1766 } catch (IOException e
) {
1767 System
.err
.println("Failed to delete received contacts temp file “" + tmpFile
+ "”: " + e
.getMessage());
1772 if (syncMessage
.getVerified().isPresent()) {
1773 final VerifiedMessage verifiedMessage
= syncMessage
.getVerified().get();
1774 account
.getSignalProtocolStore().setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage
.getDestination()), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1776 if (syncMessage
.getConfiguration().isPresent()) {
1784 private File
getContactAvatarFile(String number
) {
1785 return new File(pathConfig
.getAvatarsPath(), "contact-" + number
);
1788 private File
retrieveContactAvatarAttachment(SignalServiceAttachment attachment
, String number
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1789 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
1790 if (attachment
.isPointer()) {
1791 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1792 return retrieveAttachment(pointer
, getContactAvatarFile(number
), false);
1794 SignalServiceAttachmentStream stream
= attachment
.asStream();
1795 return Utils
.retrieveAttachment(stream
, getContactAvatarFile(number
));
1799 private File
getGroupAvatarFile(byte[] groupId
) {
1800 return new File(pathConfig
.getAvatarsPath(), "group-" + Base64
.encodeBytes(groupId
).replace("/", "_"));
1803 private File
retrieveGroupAvatarAttachment(SignalServiceAttachment attachment
, byte[] groupId
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1804 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
1805 if (attachment
.isPointer()) {
1806 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1807 return retrieveAttachment(pointer
, getGroupAvatarFile(groupId
), false);
1809 SignalServiceAttachmentStream stream
= attachment
.asStream();
1810 return Utils
.retrieveAttachment(stream
, getGroupAvatarFile(groupId
));
1814 private File
getProfileAvatarFile(SignalServiceAddress address
) {
1815 return new File(pathConfig
.getAvatarsPath(), "profile-" + address
.getLegacyIdentifier());
1818 private File
retrieveProfileAvatar(SignalServiceAddress address
, String avatarPath
, ProfileKey profileKey
) throws IOException
{
1819 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
1820 SignalServiceMessageReceiver receiver
= getMessageReceiver();
1821 File outputFile
= getProfileAvatarFile(address
);
1823 File tmpFile
= IOUtils
.createTempFile();
1824 try (InputStream input
= receiver
.retrieveProfileAvatar(avatarPath
, tmpFile
, profileKey
, ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
)) {
1825 // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
1826 IOUtils
.copyStreamToFile(input
, outputFile
, (int) ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
);
1829 Files
.delete(tmpFile
.toPath());
1830 } catch (IOException e
) {
1831 System
.err
.println("Failed to delete received avatar temp file “" + tmpFile
+ "”: " + e
.getMessage());
1837 public File
getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId
) {
1838 return new File(pathConfig
.getAttachmentsPath(), attachmentId
.toString());
1841 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1842 IOUtils
.createPrivateDirectories(pathConfig
.getAttachmentsPath());
1843 return retrieveAttachment(pointer
, getAttachmentFile(pointer
.getRemoteId()), true);
1846 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
, File outputFile
, boolean storePreview
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1847 if (storePreview
&& pointer
.getPreview().isPresent()) {
1848 File previewFile
= new File(outputFile
+ ".preview");
1849 try (OutputStream output
= new FileOutputStream(previewFile
)) {
1850 byte[] preview
= pointer
.getPreview().get();
1851 output
.write(preview
, 0, preview
.length
);
1852 } catch (FileNotFoundException e
) {
1853 e
.printStackTrace();
1858 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1860 File tmpFile
= IOUtils
.createTempFile();
1861 try (InputStream input
= messageReceiver
.retrieveAttachment(pointer
, tmpFile
, ServiceConfig
.MAX_ATTACHMENT_SIZE
)) {
1862 IOUtils
.copyStreamToFile(input
, outputFile
);
1865 Files
.delete(tmpFile
.toPath());
1866 } catch (IOException e
) {
1867 System
.err
.println("Failed to delete received attachment temp file “" + tmpFile
+ "”: " + e
.getMessage());
1873 private InputStream
retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer
, File tmpFile
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1874 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1875 return messageReceiver
.retrieveAttachment(pointer
, tmpFile
, ServiceConfig
.MAX_ATTACHMENT_SIZE
);
1878 void sendGroups() throws IOException
, UntrustedIdentityException
{
1879 File groupsFile
= IOUtils
.createTempFile();
1882 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
1883 DeviceGroupsOutputStream out
= new DeviceGroupsOutputStream(fos
);
1884 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1885 if (record instanceof GroupInfoV1
) {
1886 GroupInfoV1 groupInfo
= (GroupInfoV1
) record;
1887 out
.write(new DeviceGroup(groupInfo
.groupId
, Optional
.fromNullable(groupInfo
.name
),
1888 new ArrayList
<>(groupInfo
.getMembers()), createGroupAvatarAttachment(groupInfo
.groupId
),
1889 groupInfo
.isMember(account
.getSelfAddress()), Optional
.of(groupInfo
.messageExpirationTime
),
1890 Optional
.fromNullable(groupInfo
.color
), groupInfo
.blocked
, Optional
.fromNullable(groupInfo
.inboxPosition
), groupInfo
.archived
));
1895 if (groupsFile
.exists() && groupsFile
.length() > 0) {
1896 try (FileInputStream groupsFileStream
= new FileInputStream(groupsFile
)) {
1897 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1898 .withStream(groupsFileStream
)
1899 .withContentType("application/octet-stream")
1900 .withLength(groupsFile
.length())
1903 sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
1908 Files
.delete(groupsFile
.toPath());
1909 } catch (IOException e
) {
1910 System
.err
.println("Failed to delete groups temp file “" + groupsFile
+ "”: " + e
.getMessage());
1915 public void sendContacts() throws IOException
, UntrustedIdentityException
{
1916 File contactsFile
= IOUtils
.createTempFile();
1919 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
1920 DeviceContactsOutputStream out
= new DeviceContactsOutputStream(fos
);
1921 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1922 VerifiedMessage verifiedMessage
= null;
1923 JsonIdentityKeyStore
.Identity currentIdentity
= account
.getSignalProtocolStore().getIdentity(record.getAddress());
1924 if (currentIdentity
!= null) {
1925 verifiedMessage
= new VerifiedMessage(record.getAddress(), currentIdentity
.getIdentityKey(), currentIdentity
.getTrustLevel().toVerifiedState(), currentIdentity
.getDateAdded().getTime());
1928 ProfileKey profileKey
= account
.getProfileStore().getProfileKey(record.getAddress());
1929 out
.write(new DeviceContact(record.getAddress(), Optional
.fromNullable(record.name
),
1930 createContactAvatarAttachment(record.number
), Optional
.fromNullable(record.color
),
1931 Optional
.fromNullable(verifiedMessage
), Optional
.fromNullable(profileKey
), record.blocked
,
1932 Optional
.of(record.messageExpirationTime
),
1933 Optional
.fromNullable(record.inboxPosition
), record.archived
));
1936 if (account
.getProfileKey() != null) {
1937 // Send our own profile key as well
1938 out
.write(new DeviceContact(account
.getSelfAddress(),
1939 Optional
.absent(), Optional
.absent(),
1940 Optional
.absent(), Optional
.absent(),
1941 Optional
.of(account
.getProfileKey()),
1942 false, Optional
.absent(), Optional
.absent(), false));
1946 if (contactsFile
.exists() && contactsFile
.length() > 0) {
1947 try (FileInputStream contactsFileStream
= new FileInputStream(contactsFile
)) {
1948 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1949 .withStream(contactsFileStream
)
1950 .withContentType("application/octet-stream")
1951 .withLength(contactsFile
.length())
1954 sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
, true)));
1959 Files
.delete(contactsFile
.toPath());
1960 } catch (IOException e
) {
1961 System
.err
.println("Failed to delete contacts temp file “" + contactsFile
+ "”: " + e
.getMessage());
1966 void sendBlockedList() throws IOException
, UntrustedIdentityException
{
1967 List
<SignalServiceAddress
> addresses
= new ArrayList
<>();
1968 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1969 if (record.blocked
) {
1970 addresses
.add(record.getAddress());
1973 List
<byte[]> groupIds
= new ArrayList
<>();
1974 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1975 if (record.isBlocked()) {
1976 groupIds
.add(record.groupId
);
1979 sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
1982 private void sendVerifiedMessage(SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
) throws IOException
, UntrustedIdentityException
{
1983 VerifiedMessage verifiedMessage
= new VerifiedMessage(destination
, identityKey
, trustLevel
.toVerifiedState(), System
.currentTimeMillis());
1984 sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
1987 public List
<ContactInfo
> getContacts() {
1988 return account
.getContactStore().getContacts();
1991 public ContactInfo
getContact(String number
) {
1992 return account
.getContactStore().getContact(Util
.getSignalServiceAddressFromIdentifier(number
));
1995 public GroupInfo
getGroup(byte[] groupId
) {
1996 return account
.getGroupStore().getGroup(groupId
);
1999 public byte[] getGroupId(GroupMasterKey groupMasterKey
) {
2000 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
2001 return groupSecretParams
.getPublicParams().getGroupIdentifier().serialize();
2004 public List
<JsonIdentityKeyStore
.Identity
> getIdentities() {
2005 return account
.getSignalProtocolStore().getIdentities();
2008 public List
<JsonIdentityKeyStore
.Identity
> getIdentities(String number
) throws InvalidNumberException
{
2009 return account
.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number
));
2013 * Trust this the identity with this fingerprint
2015 * @param name username of the identity
2016 * @param fingerprint Fingerprint
2018 public boolean trustIdentityVerified(String name
, byte[] fingerprint
) throws InvalidNumberException
{
2019 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
2020 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2024 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2025 if (!Arrays
.equals(id
.getIdentityKey().serialize(), fingerprint
)) {
2029 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2031 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2032 } catch (IOException
| UntrustedIdentityException e
) {
2033 e
.printStackTrace();
2042 * Trust this the identity with this safety number
2044 * @param name username of the identity
2045 * @param safetyNumber Safety number
2047 public boolean trustIdentityVerifiedSafetyNumber(String name
, String safetyNumber
) throws InvalidNumberException
{
2048 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
2049 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2053 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2054 if (!safetyNumber
.equals(computeSafetyNumber(address
, id
.getIdentityKey()))) {
2058 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2060 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2061 } catch (IOException
| UntrustedIdentityException e
) {
2062 e
.printStackTrace();
2071 * Trust all keys of this identity without verification
2073 * @param name username of the identity
2075 public boolean trustIdentityAllKeys(String name
) {
2076 SignalServiceAddress address
= resolveSignalServiceAddress(name
);
2077 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2081 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2082 if (id
.getTrustLevel() == TrustLevel
.UNTRUSTED
) {
2083 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2085 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2086 } catch (IOException
| UntrustedIdentityException e
) {
2087 e
.printStackTrace();
2095 public String
computeSafetyNumber(SignalServiceAddress theirAddress
, IdentityKey theirIdentityKey
) {
2096 return Utils
.computeSafetyNumber(account
.getSelfAddress(), getIdentityKeyPair().getPublicKey(), theirAddress
, theirIdentityKey
);
2099 void saveAccount() {
2103 public SignalServiceAddress
canonicalizeAndResolveSignalServiceAddress(String identifier
) throws InvalidNumberException
{
2104 String canonicalizedNumber
= UuidUtil
.isUuid(identifier
) ? identifier
: Util
.canonicalizeNumber(identifier
, account
.getUsername());
2105 return resolveSignalServiceAddress(canonicalizedNumber
);
2108 public SignalServiceAddress
resolveSignalServiceAddress(String identifier
) {
2109 SignalServiceAddress address
= Util
.getSignalServiceAddressFromIdentifier(identifier
);
2111 return resolveSignalServiceAddress(address
);
2114 public SignalServiceAddress
resolveSignalServiceAddress(SignalServiceAddress address
) {
2115 if (address
.matches(account
.getSelfAddress())) {
2116 return account
.getSelfAddress();
2119 return account
.getRecipientStore().resolveServiceAddress(address
);
2123 public void close() throws IOException
{
2124 if (messagePipe
!= null) {
2125 messagePipe
.shutdown();
2129 if (unidentifiedMessagePipe
!= null) {
2130 unidentifiedMessagePipe
.shutdown();
2131 unidentifiedMessagePipe
= null;
2137 public interface ReceiveMessageHandler
{
2139 void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent decryptedContent
, Throwable e
);