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
.JsonGroupStore
;
25 import org
.asamk
.signal
.storage
.profiles
.SignalProfile
;
26 import org
.asamk
.signal
.storage
.profiles
.SignalProfileEntry
;
27 import org
.asamk
.signal
.storage
.protocol
.JsonIdentityKeyStore
;
28 import org
.asamk
.signal
.util
.IOUtils
;
29 import org
.asamk
.signal
.util
.Util
;
30 import org
.signal
.libsignal
.metadata
.InvalidMetadataMessageException
;
31 import org
.signal
.libsignal
.metadata
.InvalidMetadataVersionException
;
32 import org
.signal
.libsignal
.metadata
.ProtocolDuplicateMessageException
;
33 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyException
;
34 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyIdException
;
35 import org
.signal
.libsignal
.metadata
.ProtocolInvalidMessageException
;
36 import org
.signal
.libsignal
.metadata
.ProtocolInvalidVersionException
;
37 import org
.signal
.libsignal
.metadata
.ProtocolLegacyMessageException
;
38 import org
.signal
.libsignal
.metadata
.ProtocolNoSessionException
;
39 import org
.signal
.libsignal
.metadata
.ProtocolUntrustedIdentityException
;
40 import org
.signal
.libsignal
.metadata
.SelfSendException
;
41 import org
.signal
.libsignal
.metadata
.certificate
.InvalidCertificateException
;
42 import org
.signal
.zkgroup
.InvalidInputException
;
43 import org
.signal
.zkgroup
.profiles
.ClientZkProfileOperations
;
44 import org
.signal
.zkgroup
.profiles
.ProfileKey
;
45 import org
.whispersystems
.libsignal
.IdentityKey
;
46 import org
.whispersystems
.libsignal
.IdentityKeyPair
;
47 import org
.whispersystems
.libsignal
.InvalidKeyException
;
48 import org
.whispersystems
.libsignal
.InvalidMessageException
;
49 import org
.whispersystems
.libsignal
.InvalidVersionException
;
50 import org
.whispersystems
.libsignal
.ecc
.Curve
;
51 import org
.whispersystems
.libsignal
.ecc
.ECKeyPair
;
52 import org
.whispersystems
.libsignal
.ecc
.ECPublicKey
;
53 import org
.whispersystems
.libsignal
.state
.PreKeyRecord
;
54 import org
.whispersystems
.libsignal
.state
.SignedPreKeyRecord
;
55 import org
.whispersystems
.libsignal
.util
.KeyHelper
;
56 import org
.whispersystems
.libsignal
.util
.Medium
;
57 import org
.whispersystems
.libsignal
.util
.Pair
;
58 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
59 import org
.whispersystems
.signalservice
.api
.SignalServiceAccountManager
;
60 import org
.whispersystems
.signalservice
.api
.SignalServiceMessagePipe
;
61 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageReceiver
;
62 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageSender
;
63 import org
.whispersystems
.signalservice
.api
.crypto
.InvalidCiphertextException
;
64 import org
.whispersystems
.signalservice
.api
.crypto
.ProfileCipher
;
65 import org
.whispersystems
.signalservice
.api
.crypto
.SignalServiceCipher
;
66 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccess
;
67 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccessPair
;
68 import org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException
;
69 import org
.whispersystems
.signalservice
.api
.groupsv2
.ClientZkOperations
;
70 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Operations
;
71 import org
.whispersystems
.signalservice
.api
.messages
.SendMessageResult
;
72 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachment
;
73 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentPointer
;
74 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentRemoteId
;
75 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentStream
;
76 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceContent
;
77 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceDataMessage
;
78 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceEnvelope
;
79 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroup
;
80 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceReceiptMessage
;
81 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceStickerManifestUpload
;
82 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceStickerManifestUpload
.StickerInfo
;
83 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.BlockedListMessage
;
84 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ContactsMessage
;
85 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContact
;
86 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsInputStream
;
87 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsOutputStream
;
88 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroup
;
89 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsInputStream
;
90 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsOutputStream
;
91 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceInfo
;
92 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.RequestMessage
;
93 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SentTranscriptMessage
;
94 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SignalServiceSyncMessage
;
95 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.VerifiedMessage
;
96 import org
.whispersystems
.signalservice
.api
.profiles
.SignalServiceProfile
;
97 import org
.whispersystems
.signalservice
.api
.push
.ContactTokenDetails
;
98 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
99 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.EncapsulatedExceptions
;
100 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.MissingConfigurationException
;
101 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NetworkFailureException
;
102 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.UnregisteredUserException
;
103 import org
.whispersystems
.signalservice
.api
.util
.InvalidNumberException
;
104 import org
.whispersystems
.signalservice
.api
.util
.SleepTimer
;
105 import org
.whispersystems
.signalservice
.api
.util
.StreamDetails
;
106 import org
.whispersystems
.signalservice
.api
.util
.UptimeSleepTimer
;
107 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
108 import org
.whispersystems
.signalservice
.internal
.configuration
.SignalServiceConfiguration
;
109 import org
.whispersystems
.signalservice
.internal
.push
.SignalServiceProtos
;
110 import org
.whispersystems
.signalservice
.internal
.push
.UnsupportedDataMessageException
;
111 import org
.whispersystems
.signalservice
.internal
.push
.VerifyAccountResponse
;
112 import org
.whispersystems
.signalservice
.internal
.util
.DynamicCredentialsProvider
;
113 import org
.whispersystems
.signalservice
.internal
.util
.Hex
;
114 import org
.whispersystems
.util
.Base64
;
116 import java
.io
.Closeable
;
118 import java
.io
.FileInputStream
;
119 import java
.io
.FileNotFoundException
;
120 import java
.io
.FileOutputStream
;
121 import java
.io
.IOException
;
122 import java
.io
.InputStream
;
123 import java
.io
.OutputStream
;
125 import java
.net
.URISyntaxException
;
126 import java
.net
.URLEncoder
;
127 import java
.nio
.charset
.StandardCharsets
;
128 import java
.nio
.file
.Files
;
129 import java
.nio
.file
.Paths
;
130 import java
.nio
.file
.StandardCopyOption
;
131 import java
.util
.ArrayList
;
132 import java
.util
.Arrays
;
133 import java
.util
.Collection
;
134 import java
.util
.Collections
;
135 import java
.util
.Date
;
136 import java
.util
.HashSet
;
137 import java
.util
.LinkedList
;
138 import java
.util
.List
;
139 import java
.util
.Locale
;
140 import java
.util
.Objects
;
141 import java
.util
.Set
;
142 import java
.util
.UUID
;
143 import java
.util
.concurrent
.ExecutionException
;
144 import java
.util
.concurrent
.ExecutorService
;
145 import java
.util
.concurrent
.TimeUnit
;
146 import java
.util
.concurrent
.TimeoutException
;
147 import java
.util
.stream
.Collectors
;
148 import java
.util
.zip
.ZipEntry
;
149 import java
.util
.zip
.ZipFile
;
151 import static org
.asamk
.signal
.manager
.ServiceConfig
.capabilities
;
153 public class Manager
implements Closeable
{
155 private final SleepTimer timer
= new UptimeSleepTimer();
156 private final SignalServiceConfiguration serviceConfiguration
;
157 private final String userAgent
;
159 private final SignalAccount account
;
160 private final PathConfig pathConfig
;
161 private SignalServiceAccountManager accountManager
;
162 private SignalServiceMessagePipe messagePipe
= null;
163 private SignalServiceMessagePipe unidentifiedMessagePipe
= null;
164 private boolean discoverableByPhoneNumber
= true;
166 public Manager(SignalAccount account
, PathConfig pathConfig
, SignalServiceConfiguration serviceConfiguration
, String userAgent
) {
167 this.account
= account
;
168 this.pathConfig
= pathConfig
;
169 this.serviceConfiguration
= serviceConfiguration
;
170 this.userAgent
= userAgent
;
171 this.accountManager
= createSignalServiceAccountManager();
173 this.account
.setResolver(this::resolveSignalServiceAddress
);
176 public String
getUsername() {
177 return account
.getUsername();
180 public SignalServiceAddress
getSelfAddress() {
181 return account
.getSelfAddress();
184 private SignalServiceAccountManager
createSignalServiceAccountManager() {
185 GroupsV2Operations groupsV2Operations
;
187 groupsV2Operations
= new GroupsV2Operations(ClientZkOperations
.create(serviceConfiguration
));
188 } catch (Throwable ignored
) {
189 groupsV2Operations
= null;
191 return new SignalServiceAccountManager(serviceConfiguration
,
192 new DynamicCredentialsProvider(account
.getUuid(), account
.getUsername(), account
.getPassword(), null, account
.getDeviceId()),
198 private IdentityKeyPair
getIdentityKeyPair() {
199 return account
.getSignalProtocolStore().getIdentityKeyPair();
202 public int getDeviceId() {
203 return account
.getDeviceId();
206 private String
getMessageCachePath() {
207 return pathConfig
.getDataPath() + "/" + account
.getUsername() + ".d/msg-cache";
210 private String
getMessageCachePath(String sender
) {
211 if (sender
== null || sender
.isEmpty()) {
212 return getMessageCachePath();
215 return getMessageCachePath() + "/" + sender
.replace("/", "_");
218 private File
getMessageCacheFile(String sender
, long now
, long timestamp
) throws IOException
{
219 String cachePath
= getMessageCachePath(sender
);
220 IOUtils
.createPrivateDirectories(cachePath
);
221 return new File(cachePath
+ "/" + now
+ "_" + timestamp
);
224 public static Manager
init(String username
, String settingsPath
, SignalServiceConfiguration serviceConfiguration
, String userAgent
) throws IOException
{
225 PathConfig pathConfig
= PathConfig
.createDefault(settingsPath
);
227 if (!SignalAccount
.userExists(pathConfig
.getDataPath(), username
)) {
228 IdentityKeyPair identityKey
= KeyHelper
.generateIdentityKeyPair();
229 int registrationId
= KeyHelper
.generateRegistrationId(false);
231 ProfileKey profileKey
= KeyUtils
.createProfileKey();
232 SignalAccount account
= SignalAccount
.create(pathConfig
.getDataPath(), username
, identityKey
, registrationId
, profileKey
);
235 return new Manager(account
, pathConfig
, serviceConfiguration
, userAgent
);
238 SignalAccount account
= SignalAccount
.load(pathConfig
.getDataPath(), username
);
240 Manager m
= new Manager(account
, pathConfig
, serviceConfiguration
, userAgent
);
242 m
.migrateLegacyConfigs();
247 private void migrateLegacyConfigs() {
248 // Copy group avatars that were previously stored in the attachments folder
249 // to the new avatar folder
250 if (JsonGroupStore
.groupsWithLegacyAvatarId
.size() > 0) {
251 for (GroupInfo g
: JsonGroupStore
.groupsWithLegacyAvatarId
) {
252 File avatarFile
= getGroupAvatarFile(g
.groupId
);
253 File attachmentFile
= getAttachmentFile(new SignalServiceAttachmentRemoteId(g
.getAvatarId()));
254 if (!avatarFile
.exists() && attachmentFile
.exists()) {
256 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
257 Files
.copy(attachmentFile
.toPath(), avatarFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
258 } catch (Exception e
) {
263 JsonGroupStore
.groupsWithLegacyAvatarId
.clear();
266 if (account
.getProfileKey() == null) {
267 // Old config file, creating new profile key
268 account
.setProfileKey(KeyUtils
.createProfileKey());
271 // Store profile keys only in profile store
272 for (ContactInfo contact
: account
.getContactStore().getContacts()) {
273 String profileKeyString
= contact
.profileKey
;
274 if (profileKeyString
== null) {
277 final ProfileKey profileKey
;
279 profileKey
= new ProfileKey(Base64
.decode(profileKeyString
));
280 } catch (InvalidInputException
| IOException e
) {
283 contact
.profileKey
= null;
284 account
.getProfileStore().storeProfileKey(contact
.getAddress(), profileKey
);
288 public void checkAccountState() throws IOException
{
289 if (account
.isRegistered()) {
290 if (accountManager
.getPreKeysCount() < ServiceConfig
.PREKEY_MINIMUM_COUNT
) {
294 if (account
.getUuid() == null) {
295 account
.setUuid(accountManager
.getOwnUuid());
301 public boolean isRegistered() {
302 return account
.isRegistered();
305 public void register(boolean voiceVerification
) throws IOException
{
306 account
.setPassword(KeyUtils
.createPassword());
308 // Resetting UUID, because registering doesn't work otherwise
309 account
.setUuid(null);
310 accountManager
= createSignalServiceAccountManager();
312 if (voiceVerification
) {
313 accountManager
.requestVoiceVerificationCode(Locale
.getDefault(), Optional
.absent(), Optional
.absent());
315 accountManager
.requestSmsVerificationCode(false, Optional
.absent(), Optional
.absent());
318 account
.setRegistered(false);
322 public void updateAccountAttributes() throws IOException
{
323 accountManager
.setAccountAttributes(account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, account
.getRegistrationLockPin(), account
.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities
, discoverableByPhoneNumber
);
326 public void setProfile(String name
, File avatar
) throws IOException
{
327 try (final StreamDetails streamDetails
= avatar
== null ?
null : Utils
.createStreamDetailsFromFile(avatar
)) {
328 accountManager
.setVersionedProfile(account
.getUuid(), account
.getProfileKey(), name
, streamDetails
);
332 public void unregister() throws IOException
{
333 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
334 // If this is the master device, other users can't send messages to this number anymore.
335 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
336 accountManager
.setGcmId(Optional
.absent());
338 account
.setRegistered(false);
342 public List
<DeviceInfo
> getLinkedDevices() throws IOException
{
343 List
<DeviceInfo
> devices
= accountManager
.getDevices();
344 account
.setMultiDevice(devices
.size() > 1);
349 public void removeLinkedDevices(int deviceId
) throws IOException
{
350 accountManager
.removeDevice(deviceId
);
351 List
<DeviceInfo
> devices
= accountManager
.getDevices();
352 account
.setMultiDevice(devices
.size() > 1);
356 public void addDeviceLink(URI linkUri
) throws IOException
, InvalidKeyException
{
357 Utils
.DeviceLinkInfo info
= Utils
.parseDeviceLinkUri(linkUri
);
359 addDevice(info
.deviceIdentifier
, info
.deviceKey
);
362 private void addDevice(String deviceIdentifier
, ECPublicKey deviceKey
) throws IOException
, InvalidKeyException
{
363 IdentityKeyPair identityKeyPair
= getIdentityKeyPair();
364 String verificationCode
= accountManager
.getNewDeviceVerificationCode();
366 accountManager
.addDevice(deviceIdentifier
, deviceKey
, identityKeyPair
, Optional
.of(account
.getProfileKey().serialize()), verificationCode
);
367 account
.setMultiDevice(true);
371 private List
<PreKeyRecord
> generatePreKeys() {
372 List
<PreKeyRecord
> records
= new ArrayList
<>(ServiceConfig
.PREKEY_BATCH_SIZE
);
374 final int offset
= account
.getPreKeyIdOffset();
375 for (int i
= 0; i
< ServiceConfig
.PREKEY_BATCH_SIZE
; i
++) {
376 int preKeyId
= (offset
+ i
) % Medium
.MAX_VALUE
;
377 ECKeyPair keyPair
= Curve
.generateKeyPair();
378 PreKeyRecord
record = new PreKeyRecord(preKeyId
, keyPair
);
383 account
.addPreKeys(records
);
389 private SignedPreKeyRecord
generateSignedPreKey(IdentityKeyPair identityKeyPair
) {
391 ECKeyPair keyPair
= Curve
.generateKeyPair();
392 byte[] signature
= Curve
.calculateSignature(identityKeyPair
.getPrivateKey(), keyPair
.getPublicKey().serialize());
393 SignedPreKeyRecord
record = new SignedPreKeyRecord(account
.getNextSignedPreKeyId(), System
.currentTimeMillis(), keyPair
, signature
);
395 account
.addSignedPreKey(record);
399 } catch (InvalidKeyException e
) {
400 throw new AssertionError(e
);
404 public void verifyAccount(String verificationCode
, String pin
) throws IOException
{
405 verificationCode
= verificationCode
.replace("-", "");
406 account
.setSignalingKey(KeyUtils
.createSignalingKey());
407 // TODO make unrestricted unidentified access configurable
408 VerifyAccountResponse response
= accountManager
.verifyAccountWithCode(verificationCode
, account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, pin
, null, getSelfUnidentifiedAccessKey(), false, capabilities
, discoverableByPhoneNumber
);
410 UUID uuid
= UuidUtil
.parseOrNull(response
.getUuid());
411 // TODO response.isStorageCapable()
412 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
413 account
.setRegistered(true);
414 account
.setUuid(uuid
);
415 account
.setRegistrationLockPin(pin
);
416 account
.getSignalProtocolStore().saveIdentity(account
.getSelfAddress(), getIdentityKeyPair().getPublicKey(), TrustLevel
.TRUSTED_VERIFIED
);
422 public void setRegistrationLockPin(Optional
<String
> pin
) throws IOException
{
423 if (pin
.isPresent()) {
424 account
.setRegistrationLockPin(pin
.get());
425 throw new RuntimeException("Not implemented anymore, will be replaced with KBS");
427 account
.setRegistrationLockPin(null);
428 accountManager
.removeRegistrationLockV1();
433 void refreshPreKeys() throws IOException
{
434 List
<PreKeyRecord
> oneTimePreKeys
= generatePreKeys();
435 final IdentityKeyPair identityKeyPair
= getIdentityKeyPair();
436 SignedPreKeyRecord signedPreKeyRecord
= generateSignedPreKey(identityKeyPair
);
438 accountManager
.setPreKeys(identityKeyPair
.getPublicKey(), signedPreKeyRecord
, oneTimePreKeys
);
441 private SignalServiceMessageReceiver
getMessageReceiver() {
442 // TODO implement ZkGroup support
443 final ClientZkProfileOperations clientZkProfileOperations
= null;
444 return new SignalServiceMessageReceiver(serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), account
.getSignalingKey(), userAgent
, null, timer
, clientZkProfileOperations
);
447 private SignalServiceMessageSender
getMessageSender() {
448 // TODO implement ZkGroup support
449 final ClientZkProfileOperations clientZkProfileOperations
= null;
450 final ExecutorService executor
= null;
451 return new SignalServiceMessageSender(serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(),
452 account
.getDeviceId(), account
.getSignalProtocolStore(), userAgent
, account
.isMultiDevice(), Optional
.fromNullable(messagePipe
), Optional
.fromNullable(unidentifiedMessagePipe
), Optional
.absent(), clientZkProfileOperations
, executor
, ServiceConfig
.MAX_ENVELOPE_SIZE
);
455 private SignalServiceProfile
getEncryptedRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
) throws IOException
{
456 SignalServiceMessagePipe pipe
= unidentifiedMessagePipe
!= null && unidentifiedAccess
.isPresent() ? unidentifiedMessagePipe
461 return pipe
.getProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).get(10, TimeUnit
.SECONDS
).getProfile();
462 } catch (IOException
| InterruptedException
| ExecutionException
| TimeoutException ignored
) {
466 SignalServiceMessageReceiver receiver
= getMessageReceiver();
468 return receiver
.retrieveProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).get(10, TimeUnit
.SECONDS
).getProfile();
469 } catch (InterruptedException
| ExecutionException
| TimeoutException e
) {
470 throw new IOException("Failed to retrieve profile", e
);
474 private SignalProfile
getRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
, ProfileKey profileKey
) throws IOException
{
475 SignalProfileEntry profileEntry
= account
.getProfileStore().getProfile(address
);
476 long now
= new Date().getTime();
477 // Profiles are cache for 24h before retrieving them again
478 if (profileEntry
== null || profileEntry
.getProfile() == null || now
- profileEntry
.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000) {
479 SignalProfile profile
= retrieveRecipientProfile(address
, unidentifiedAccess
, profileKey
);
480 account
.getProfileStore().updateProfile(address
, profileKey
, now
, profile
);
483 return profileEntry
.getProfile();
486 private SignalProfile
retrieveRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
, ProfileKey profileKey
) throws IOException
{
487 final SignalServiceProfile encryptedProfile
= getEncryptedRecipientProfile(address
, unidentifiedAccess
);
489 File avatarFile
= null;
491 avatarFile
= encryptedProfile
.getAvatar() == null ?
null : retrieveProfileAvatar(address
, encryptedProfile
.getAvatar(), profileKey
);
492 } catch (Throwable e
) {
493 System
.err
.println("Failed to retrieve profile avatar, ignoring: " + e
.getMessage());
496 ProfileCipher profileCipher
= new ProfileCipher(profileKey
);
498 return new SignalProfile(
499 encryptedProfile
.getIdentityKey(),
500 encryptedProfile
.getName() == null ?
null : new String(profileCipher
.decryptName(Base64
.decode(encryptedProfile
.getName()))),
502 encryptedProfile
.getUnidentifiedAccess() == null || !profileCipher
.verifyUnidentifiedAccess(Base64
.decode(encryptedProfile
.getUnidentifiedAccess())) ?
null : encryptedProfile
.getUnidentifiedAccess(),
503 encryptedProfile
.isUnrestrictedUnidentifiedAccess(),
504 encryptedProfile
.getCapabilities());
505 } catch (InvalidCiphertextException e
) {
510 private Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(byte[] groupId
) throws IOException
{
511 File file
= getGroupAvatarFile(groupId
);
512 if (!file
.exists()) {
513 return Optional
.absent();
516 return Optional
.of(Utils
.createAttachment(file
));
519 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(String number
) throws IOException
{
520 File file
= getContactAvatarFile(number
);
521 if (!file
.exists()) {
522 return Optional
.absent();
525 return Optional
.of(Utils
.createAttachment(file
));
528 private GroupInfo
getGroupForSending(byte[] groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
529 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
531 throw new GroupNotFoundException(groupId
);
533 if (!g
.isMember(account
.getSelfAddress())) {
534 throw new NotAGroupMemberException(groupId
, g
.name
);
539 public List
<GroupInfo
> getGroups() {
540 return account
.getGroupStore().getGroups();
543 public long sendGroupMessage(String messageText
, List
<String
> attachments
,
545 throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
, NotAGroupMemberException
{
546 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
547 if (attachments
!= null) {
548 messageBuilder
.withAttachments(Utils
.getSignalServiceAttachments(attachments
));
550 if (groupId
!= null) {
551 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
554 messageBuilder
.asGroupMessage(group
);
557 final GroupInfo g
= getGroupForSending(groupId
);
559 messageBuilder
.withExpiration(g
.messageExpirationTime
);
561 return sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
564 public void sendGroupMessageReaction(String emoji
, boolean remove
, String targetAuthor
,
565 long targetSentTimestamp
, byte[] groupId
)
566 throws IOException
, EncapsulatedExceptions
, InvalidNumberException
, NotAGroupMemberException
, GroupNotFoundException
{
567 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, canonicalizeAndResolveSignalServiceAddress(targetAuthor
), targetSentTimestamp
);
568 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
569 .withReaction(reaction
);
570 if (groupId
!= null) {
571 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
574 messageBuilder
.asGroupMessage(group
);
576 final GroupInfo g
= getGroupForSending(groupId
);
577 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
580 public void sendQuitGroupMessage(byte[] groupId
) throws GroupNotFoundException
, IOException
, EncapsulatedExceptions
, NotAGroupMemberException
{
581 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
585 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
586 .asGroupMessage(group
);
588 final GroupInfo g
= getGroupForSending(groupId
);
589 g
.removeMember(account
.getSelfAddress());
590 account
.getGroupStore().updateGroup(g
);
592 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
595 private byte[] sendUpdateGroupMessage(byte[] groupId
, String name
, Collection
<SignalServiceAddress
> members
, String avatarFile
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
, NotAGroupMemberException
{
597 if (groupId
== null) {
599 g
= new GroupInfo(KeyUtils
.createGroupId());
600 g
.addMembers(Collections
.singleton(account
.getSelfAddress()));
602 g
= getGroupForSending(groupId
);
609 if (members
!= null) {
610 final Set
<String
> newE164Members
= new HashSet
<>();
611 for (SignalServiceAddress member
: members
) {
612 if (g
.isMember(member
) || !member
.getNumber().isPresent()) {
615 newE164Members
.add(member
.getNumber().get());
618 final List
<ContactTokenDetails
> contacts
= accountManager
.getContacts(newE164Members
);
619 if (contacts
.size() != newE164Members
.size()) {
620 // Some of the new members are not registered on Signal
621 for (ContactTokenDetails contact
: contacts
) {
622 newE164Members
.remove(contact
.getNumber());
624 throw new IOException("Failed to add members " + Util
.join(", ", newE164Members
) + " to group: Not registered on Signal");
627 g
.addMembers(members
);
630 if (avatarFile
!= null) {
631 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
632 File aFile
= getGroupAvatarFile(g
.groupId
);
633 Files
.copy(Paths
.get(avatarFile
), aFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
636 account
.getGroupStore().updateGroup(g
);
638 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
640 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
644 void sendUpdateGroupMessage(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
, NotAGroupMemberException
, GroupNotFoundException
, AttachmentInvalidException
{
645 if (groupId
== null) {
648 GroupInfo g
= getGroupForSending(groupId
);
650 if (!g
.isMember(recipient
)) {
654 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
656 // Send group message only to the recipient who requested it
657 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
660 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfo g
) throws AttachmentInvalidException
{
661 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
664 .withMembers(new ArrayList
<>(g
.getMembers()));
666 File aFile
= getGroupAvatarFile(g
.groupId
);
667 if (aFile
.exists()) {
669 group
.withAvatar(Utils
.createAttachment(aFile
));
670 } catch (IOException e
) {
671 throw new AttachmentInvalidException(aFile
.toString(), e
);
675 return SignalServiceDataMessage
.newBuilder()
676 .asGroupMessage(group
.build())
677 .withExpiration(g
.messageExpirationTime
);
680 void sendGroupInfoRequest(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
681 if (groupId
== null) {
685 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
)
688 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
689 .asGroupMessage(group
.build());
691 // Send group info request message to the recipient who sent us a message with this groupId
692 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
695 void sendReceipt(SignalServiceAddress remoteAddress
, long messageId
) throws IOException
, UntrustedIdentityException
{
696 SignalServiceReceiptMessage receiptMessage
= new SignalServiceReceiptMessage(SignalServiceReceiptMessage
.Type
.DELIVERY
,
697 Collections
.singletonList(messageId
),
698 System
.currentTimeMillis());
700 getMessageSender().sendReceipt(remoteAddress
, getAccessFor(remoteAddress
), receiptMessage
);
703 public long sendMessage(String messageText
, List
<String
> attachments
,
704 List
<String
> recipients
)
705 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
706 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
707 if (attachments
!= null) {
708 List
<SignalServiceAttachment
> attachmentStreams
= Utils
.getSignalServiceAttachments(attachments
);
710 // Upload attachments here, so we only upload once even for multiple recipients
711 SignalServiceMessageSender messageSender
= getMessageSender();
712 List
<SignalServiceAttachment
> attachmentPointers
= new ArrayList
<>(attachmentStreams
.size());
713 for (SignalServiceAttachment attachment
: attachmentStreams
) {
714 if (attachment
.isStream()) {
715 attachmentPointers
.add(messageSender
.uploadAttachment(attachment
.asStream()));
716 } else if (attachment
.isPointer()) {
717 attachmentPointers
.add(attachment
.asPointer());
721 messageBuilder
.withAttachments(attachmentPointers
);
723 return sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
726 public void sendMessageReaction(String emoji
, boolean remove
, String targetAuthor
,
727 long targetSentTimestamp
, List
<String
> recipients
)
728 throws IOException
, EncapsulatedExceptions
, InvalidNumberException
{
729 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, canonicalizeAndResolveSignalServiceAddress(targetAuthor
), targetSentTimestamp
);
730 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
731 .withReaction(reaction
);
732 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
735 public void sendEndSessionMessage(List
<String
> recipients
) throws IOException
, EncapsulatedExceptions
, InvalidNumberException
{
736 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
737 .asEndSessionMessage();
739 final Collection
<SignalServiceAddress
> signalServiceAddresses
= getSignalServiceAddresses(recipients
);
741 sendMessageLegacy(messageBuilder
, signalServiceAddresses
);
742 } catch (Exception e
) {
743 for (SignalServiceAddress address
: signalServiceAddresses
) {
744 handleEndSession(address
);
751 public String
getContactName(String number
) throws InvalidNumberException
{
752 ContactInfo contact
= account
.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number
));
753 if (contact
== null) {
760 public void setContactName(String number
, String name
) throws InvalidNumberException
{
761 final SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(number
);
762 ContactInfo contact
= account
.getContactStore().getContact(address
);
763 if (contact
== null) {
764 contact
= new ContactInfo(address
);
767 account
.getContactStore().updateContact(contact
);
771 public void setContactBlocked(String number
, boolean blocked
) throws InvalidNumberException
{
772 setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number
), blocked
);
775 private void setContactBlocked(SignalServiceAddress address
, boolean blocked
) {
776 ContactInfo contact
= account
.getContactStore().getContact(address
);
777 if (contact
== null) {
778 contact
= new ContactInfo(address
);
780 contact
.blocked
= blocked
;
781 account
.getContactStore().updateContact(contact
);
785 public void setGroupBlocked(final byte[] groupId
, final boolean blocked
) throws GroupNotFoundException
{
786 GroupInfo group
= getGroup(groupId
);
788 throw new GroupNotFoundException(groupId
);
791 group
.blocked
= blocked
;
792 account
.getGroupStore().updateGroup(group
);
796 public byte[] updateGroup(byte[] groupId
, String name
, List
<String
> members
, String avatar
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
, InvalidNumberException
, NotAGroupMemberException
{
797 if (groupId
.length
== 0) {
800 if (name
.isEmpty()) {
803 if (members
.isEmpty()) {
806 if (avatar
.isEmpty()) {
809 return sendUpdateGroupMessage(groupId
, name
, members
== null ?
null : getSignalServiceAddresses(members
), avatar
);
813 * Change the expiration timer for a contact
815 public void setExpirationTimer(SignalServiceAddress address
, int messageExpirationTimer
) throws IOException
{
816 ContactInfo contact
= account
.getContactStore().getContact(address
);
817 contact
.messageExpirationTime
= messageExpirationTimer
;
818 account
.getContactStore().updateContact(contact
);
819 sendExpirationTimerUpdate(address
);
823 private void sendExpirationTimerUpdate(SignalServiceAddress address
) throws IOException
{
824 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
825 .asExpirationUpdate();
826 sendMessage(messageBuilder
, Collections
.singleton(address
));
830 * Change the expiration timer for a contact
832 public void setExpirationTimer(String number
, int messageExpirationTimer
) throws IOException
, InvalidNumberException
{
833 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(number
);
834 setExpirationTimer(address
, messageExpirationTimer
);
838 * Change the expiration timer for a group
840 public void setExpirationTimer(byte[] groupId
, int messageExpirationTimer
) {
841 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
842 g
.messageExpirationTime
= messageExpirationTimer
;
843 account
.getGroupStore().updateGroup(g
);
847 * Upload the sticker pack from path.
849 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
850 * @return if successful, returns the URL to install the sticker pack in the signal app
852 public String
uploadStickerPack(String path
) throws IOException
, StickerPackInvalidException
{
853 SignalServiceStickerManifestUpload manifest
= getSignalServiceStickerManifestUpload(path
);
855 SignalServiceMessageSender messageSender
= getMessageSender();
857 byte[] packKey
= KeyUtils
.createStickerUploadKey();
858 String packId
= messageSender
.uploadStickerManifest(manifest
, packKey
);
861 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
))
863 } catch (URISyntaxException e
) {
864 throw new AssertionError(e
);
868 private SignalServiceStickerManifestUpload
getSignalServiceStickerManifestUpload(final String path
) throws IOException
, StickerPackInvalidException
{
870 String rootPath
= null;
872 final File file
= new File(path
);
873 if (file
.getName().endsWith(".zip")) {
874 zip
= new ZipFile(file
);
875 } else if (file
.getName().equals("manifest.json")) {
876 rootPath
= file
.getParent();
878 throw new StickerPackInvalidException("Could not find manifest.json");
881 JsonStickerPack pack
= parseStickerPack(rootPath
, zip
);
883 if (pack
.stickers
== null) {
884 throw new StickerPackInvalidException("Must set a 'stickers' field.");
887 if (pack
.stickers
.isEmpty()) {
888 throw new StickerPackInvalidException("Must include stickers.");
891 List
<StickerInfo
> stickers
= new ArrayList
<>(pack
.stickers
.size());
892 for (JsonStickerPack
.JsonSticker sticker
: pack
.stickers
) {
893 if (sticker
.file
== null) {
894 throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
897 Pair
<InputStream
, Long
> data
;
899 data
= getInputStreamAndLength(rootPath
, zip
, sticker
.file
);
900 } catch (IOException ignored
) {
901 throw new StickerPackInvalidException("Could not find find " + sticker
.file
);
904 String contentType
= Utils
.getFileMimeType(new File(sticker
.file
), null);
905 StickerInfo stickerInfo
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(sticker
.emoji
).or(""), contentType
);
906 stickers
.add(stickerInfo
);
909 StickerInfo cover
= null;
910 if (pack
.cover
!= null) {
911 if (pack
.cover
.file
== null) {
912 throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
915 Pair
<InputStream
, Long
> data
;
917 data
= getInputStreamAndLength(rootPath
, zip
, pack
.cover
.file
);
918 } catch (IOException ignored
) {
919 throw new StickerPackInvalidException("Could not find find " + pack
.cover
.file
);
922 String contentType
= Utils
.getFileMimeType(new File(pack
.cover
.file
), null);
923 cover
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(pack
.cover
.emoji
).or(""), contentType
);
926 return new SignalServiceStickerManifestUpload(
933 private static JsonStickerPack
parseStickerPack(String rootPath
, ZipFile zip
) throws IOException
{
934 InputStream inputStream
;
936 inputStream
= zip
.getInputStream(zip
.getEntry("manifest.json"));
938 inputStream
= new FileInputStream((new File(rootPath
, "manifest.json")));
940 return new ObjectMapper().readValue(inputStream
, JsonStickerPack
.class);
943 private static Pair
<InputStream
, Long
> getInputStreamAndLength(final String rootPath
, final ZipFile zip
, final String subfile
) throws IOException
{
945 final ZipEntry entry
= zip
.getEntry(subfile
);
946 return new Pair
<>(zip
.getInputStream(entry
), entry
.getSize());
948 final File file
= new File(rootPath
, subfile
);
949 return new Pair
<>(new FileInputStream(file
), file
.length());
953 void requestSyncGroups() throws IOException
{
954 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
).build();
955 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
957 sendSyncMessage(message
);
958 } catch (UntrustedIdentityException e
) {
963 void requestSyncContacts() throws IOException
{
964 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
).build();
965 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
967 sendSyncMessage(message
);
968 } catch (UntrustedIdentityException e
) {
973 void requestSyncBlocked() throws IOException
{
974 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
).build();
975 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
977 sendSyncMessage(message
);
978 } catch (UntrustedIdentityException e
) {
983 void requestSyncConfiguration() throws IOException
{
984 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
).build();
985 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
987 sendSyncMessage(message
);
988 } catch (UntrustedIdentityException e
) {
993 private byte[] getSenderCertificate() {
994 // TODO support UUID capable sender certificates
995 // byte[] certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy();
998 certificate
= accountManager
.getSenderCertificate();
999 } catch (IOException e
) {
1000 System
.err
.println("Failed to get sender certificate: " + e
);
1003 // TODO cache for a day
1007 private byte[] getSelfUnidentifiedAccessKey() {
1008 return UnidentifiedAccess
.deriveAccessKeyFrom(account
.getProfileKey());
1011 private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient
) {
1012 ProfileKey theirProfileKey
= account
.getProfileStore().getProfileKey(recipient
);
1013 if (theirProfileKey
== null) {
1016 SignalProfile targetProfile
;
1018 targetProfile
= getRecipientProfile(recipient
, Optional
.absent(), theirProfileKey
);
1019 } catch (IOException e
) {
1020 System
.err
.println("Failed to get recipient profile: " + e
);
1024 if (targetProfile
== null || targetProfile
.getUnidentifiedAccess() == null) {
1028 if (targetProfile
.isUnrestrictedUnidentifiedAccess()) {
1029 return KeyUtils
.createUnrestrictedUnidentifiedAccess();
1032 return UnidentifiedAccess
.deriveAccessKeyFrom(theirProfileKey
);
1035 private Optional
<UnidentifiedAccessPair
> getAccessForSync() {
1036 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1037 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1039 if (selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1040 return Optional
.absent();
1044 return Optional
.of(new UnidentifiedAccessPair(
1045 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1046 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1048 } catch (InvalidCertificateException e
) {
1049 return Optional
.absent();
1053 private List
<Optional
<UnidentifiedAccessPair
>> getAccessFor(Collection
<SignalServiceAddress
> recipients
) {
1054 List
<Optional
<UnidentifiedAccessPair
>> result
= new ArrayList
<>(recipients
.size());
1055 for (SignalServiceAddress recipient
: recipients
) {
1056 result
.add(getAccessFor(recipient
));
1061 private Optional
<UnidentifiedAccessPair
> getAccessFor(SignalServiceAddress recipient
) {
1062 byte[] recipientUnidentifiedAccessKey
= getTargetUnidentifiedAccessKey(recipient
);
1063 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1064 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1066 if (recipientUnidentifiedAccessKey
== null || selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1067 return Optional
.absent();
1071 return Optional
.of(new UnidentifiedAccessPair(
1072 new UnidentifiedAccess(recipientUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1073 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1075 } catch (InvalidCertificateException e
) {
1076 return Optional
.absent();
1080 private Optional
<UnidentifiedAccess
> getUnidentifiedAccess(SignalServiceAddress recipient
) {
1081 Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1083 if (unidentifiedAccess
.isPresent()) {
1084 return unidentifiedAccess
.get().getTargetUnidentifiedAccess();
1087 return Optional
.absent();
1090 private void sendSyncMessage(SignalServiceSyncMessage message
)
1091 throws IOException
, UntrustedIdentityException
{
1092 SignalServiceMessageSender messageSender
= getMessageSender();
1094 messageSender
.sendMessage(message
, getAccessForSync());
1095 } catch (UntrustedIdentityException e
) {
1096 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1102 * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult.
1104 private long sendMessageLegacy(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1105 throws EncapsulatedExceptions
, IOException
{
1106 final long timestamp
= System
.currentTimeMillis();
1107 messageBuilder
.withTimestamp(timestamp
);
1108 List
<SendMessageResult
> results
= sendMessage(messageBuilder
, recipients
);
1110 List
<UntrustedIdentityException
> untrustedIdentities
= new LinkedList
<>();
1111 List
<UnregisteredUserException
> unregisteredUsers
= new LinkedList
<>();
1112 List
<NetworkFailureException
> networkExceptions
= new LinkedList
<>();
1114 for (SendMessageResult result
: results
) {
1115 if (result
.isUnregisteredFailure()) {
1116 unregisteredUsers
.add(new UnregisteredUserException(result
.getAddress().getLegacyIdentifier(), null));
1117 } else if (result
.isNetworkFailure()) {
1118 networkExceptions
.add(new NetworkFailureException(result
.getAddress().getLegacyIdentifier(), null));
1119 } else if (result
.getIdentityFailure() != null) {
1120 untrustedIdentities
.add(new UntrustedIdentityException("Untrusted", result
.getAddress().getLegacyIdentifier(), result
.getIdentityFailure().getIdentityKey()));
1123 if (!untrustedIdentities
.isEmpty() || !unregisteredUsers
.isEmpty() || !networkExceptions
.isEmpty()) {
1124 throw new EncapsulatedExceptions(untrustedIdentities
, unregisteredUsers
, networkExceptions
);
1129 private Collection
<SignalServiceAddress
> getSignalServiceAddresses(Collection
<String
> numbers
) throws InvalidNumberException
{
1130 final Set
<SignalServiceAddress
> signalServiceAddresses
= new HashSet
<>(numbers
.size());
1132 for (String number
: numbers
) {
1133 signalServiceAddresses
.add(canonicalizeAndResolveSignalServiceAddress(number
));
1135 return signalServiceAddresses
;
1138 private List
<SendMessageResult
> sendMessage(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1139 throws IOException
{
1140 if (messagePipe
== null) {
1141 messagePipe
= getMessageReceiver().createMessagePipe();
1143 if (unidentifiedMessagePipe
== null) {
1144 unidentifiedMessagePipe
= getMessageReceiver().createUnidentifiedMessagePipe();
1146 SignalServiceDataMessage message
= null;
1148 message
= messageBuilder
.build();
1149 if (message
.getGroupContext().isPresent()) {
1151 SignalServiceMessageSender messageSender
= getMessageSender();
1152 final boolean isRecipientUpdate
= false;
1153 List
<SendMessageResult
> result
= messageSender
.sendMessage(new ArrayList
<>(recipients
), getAccessFor(recipients
), isRecipientUpdate
, message
);
1154 for (SendMessageResult r
: result
) {
1155 if (r
.getIdentityFailure() != null) {
1156 account
.getSignalProtocolStore().saveIdentity(r
.getAddress(), r
.getIdentityFailure().getIdentityKey(), TrustLevel
.UNTRUSTED
);
1160 } catch (UntrustedIdentityException e
) {
1161 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1162 return Collections
.emptyList();
1165 // Send to all individually, so sync messages are sent correctly
1166 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1167 for (SignalServiceAddress address
: recipients
) {
1168 ContactInfo contact
= account
.getContactStore().getContact(address
);
1169 if (contact
!= null) {
1170 messageBuilder
.withExpiration(contact
.messageExpirationTime
);
1171 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
1173 messageBuilder
.withExpiration(0);
1174 messageBuilder
.withProfileKey(null);
1176 message
= messageBuilder
.build();
1177 if (address
.matches(account
.getSelfAddress())) {
1178 results
.add(sendSelfMessage(message
));
1180 results
.add(sendMessage(address
, message
));
1186 if (message
!= null && message
.isEndSession()) {
1187 for (SignalServiceAddress recipient
: recipients
) {
1188 handleEndSession(recipient
);
1195 private SendMessageResult
sendSelfMessage(SignalServiceDataMessage message
) throws IOException
{
1196 SignalServiceMessageSender messageSender
= getMessageSender();
1198 SignalServiceAddress recipient
= account
.getSelfAddress();
1200 final Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1201 SentTranscriptMessage transcript
= new SentTranscriptMessage(Optional
.of(recipient
),
1202 message
.getTimestamp(),
1204 message
.getExpiresInSeconds(),
1205 Collections
.singletonMap(recipient
, unidentifiedAccess
.isPresent()),
1207 SignalServiceSyncMessage syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
1210 long startTime
= System
.currentTimeMillis();
1211 messageSender
.sendMessage(syncMessage
, unidentifiedAccess
);
1212 return SendMessageResult
.success(recipient
, unidentifiedAccess
.isPresent(), false, System
.currentTimeMillis() - startTime
);
1213 } catch (UntrustedIdentityException e
) {
1214 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1215 return SendMessageResult
.identityFailure(recipient
, e
.getIdentityKey());
1219 private SendMessageResult
sendMessage(SignalServiceAddress address
, SignalServiceDataMessage message
) throws IOException
{
1220 SignalServiceMessageSender messageSender
= getMessageSender();
1223 return messageSender
.sendMessage(address
, getAccessFor(address
), message
);
1224 } catch (UntrustedIdentityException e
) {
1225 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1226 return SendMessageResult
.identityFailure(address
, e
.getIdentityKey());
1230 private SignalServiceContent
decryptMessage(SignalServiceEnvelope envelope
) throws InvalidMetadataMessageException
, ProtocolInvalidMessageException
, ProtocolDuplicateMessageException
, ProtocolLegacyMessageException
, ProtocolInvalidKeyIdException
, InvalidMetadataVersionException
, ProtocolInvalidVersionException
, ProtocolNoSessionException
, ProtocolInvalidKeyException
, SelfSendException
, UnsupportedDataMessageException
, org
.whispersystems
.libsignal
.UntrustedIdentityException
{
1231 SignalServiceCipher cipher
= new SignalServiceCipher(account
.getSelfAddress(), account
.getSignalProtocolStore(), Utils
.getCertificateValidator());
1233 return cipher
.decrypt(envelope
);
1234 } catch (ProtocolUntrustedIdentityException e
) {
1235 if (e
.getCause() instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
) {
1236 org
.whispersystems
.libsignal
.UntrustedIdentityException identityException
= (org
.whispersystems
.libsignal
.UntrustedIdentityException
) e
.getCause();
1237 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(identityException
.getName()), identityException
.getUntrustedIdentity(), TrustLevel
.UNTRUSTED
);
1238 throw identityException
;
1240 throw new AssertionError(e
);
1244 private void handleEndSession(SignalServiceAddress source
) {
1245 account
.getSignalProtocolStore().deleteAllSessions(source
);
1248 private List
<HandleAction
> handleSignalServiceDataMessage(SignalServiceDataMessage message
, boolean isSync
, SignalServiceAddress source
, SignalServiceAddress destination
, boolean ignoreAttachments
) {
1249 List
<HandleAction
> actions
= new ArrayList
<>();
1250 if (message
.getGroupContext().isPresent() && message
.getGroupContext().get().getGroupV1().isPresent()) {
1251 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1252 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1253 switch (groupInfo
.getType()) {
1255 if (group
== null) {
1256 group
= new GroupInfo(groupInfo
.getGroupId());
1259 if (groupInfo
.getAvatar().isPresent()) {
1260 SignalServiceAttachment avatar
= groupInfo
.getAvatar().get();
1261 if (avatar
.isPointer()) {
1263 retrieveGroupAvatarAttachment(avatar
.asPointer(), group
.groupId
);
1264 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1265 System
.err
.println("Failed to retrieve group avatar (" + avatar
.asPointer().getRemoteId() + "): " + e
.getMessage());
1270 if (groupInfo
.getName().isPresent()) {
1271 group
.name
= groupInfo
.getName().get();
1274 if (groupInfo
.getMembers().isPresent()) {
1275 group
.addMembers(groupInfo
.getMembers().get()
1277 .map(this::resolveSignalServiceAddress
)
1278 .collect(Collectors
.toSet()));
1281 account
.getGroupStore().updateGroup(group
);
1284 if (group
== null && !isSync
) {
1285 actions
.add(new SendGroupInfoRequestAction(source
, groupInfo
.getGroupId()));
1289 if (group
!= null) {
1290 group
.removeMember(source
);
1291 account
.getGroupStore().updateGroup(group
);
1295 if (group
!= null && !isSync
) {
1296 actions
.add(new SendGroupUpdateAction(source
, group
.groupId
));
1301 final SignalServiceAddress conversationPartnerAddress
= isSync ? destination
: source
;
1302 if (message
.isEndSession()) {
1303 handleEndSession(conversationPartnerAddress
);
1305 if (message
.isExpirationUpdate() || message
.getBody().isPresent()) {
1306 if (message
.getGroupContext().isPresent() && message
.getGroupContext().get().getGroupV1().isPresent()) {
1307 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1308 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1309 if (group
== null) {
1310 group
= new GroupInfo(groupInfo
.getGroupId());
1312 if (group
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1313 group
.messageExpirationTime
= message
.getExpiresInSeconds();
1314 account
.getGroupStore().updateGroup(group
);
1317 ContactInfo contact
= account
.getContactStore().getContact(conversationPartnerAddress
);
1318 if (contact
== null) {
1319 contact
= new ContactInfo(conversationPartnerAddress
);
1321 if (contact
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1322 contact
.messageExpirationTime
= message
.getExpiresInSeconds();
1323 account
.getContactStore().updateContact(contact
);
1327 if (message
.getAttachments().isPresent() && !ignoreAttachments
) {
1328 for (SignalServiceAttachment attachment
: message
.getAttachments().get()) {
1329 if (attachment
.isPointer()) {
1331 retrieveAttachment(attachment
.asPointer());
1332 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1333 System
.err
.println("Failed to retrieve attachment (" + attachment
.asPointer().getRemoteId() + "): " + e
.getMessage());
1338 if (message
.getProfileKey().isPresent() && message
.getProfileKey().get().length
== 32) {
1339 final ProfileKey profileKey
;
1341 profileKey
= new ProfileKey(message
.getProfileKey().get());
1342 } catch (InvalidInputException e
) {
1343 throw new AssertionError(e
);
1345 if (source
.matches(account
.getSelfAddress())) {
1346 this.account
.setProfileKey(profileKey
);
1348 this.account
.getProfileStore().storeProfileKey(source
, profileKey
);
1350 if (message
.getPreviews().isPresent()) {
1351 final List
<SignalServiceDataMessage
.Preview
> previews
= message
.getPreviews().get();
1352 for (SignalServiceDataMessage
.Preview preview
: previews
) {
1353 if (preview
.getImage().isPresent() && preview
.getImage().get().isPointer()) {
1354 SignalServiceAttachmentPointer attachment
= preview
.getImage().get().asPointer();
1356 retrieveAttachment(attachment
);
1357 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1358 System
.err
.println("Failed to retrieve attachment (" + attachment
.getRemoteId() + "): " + e
.getMessage());
1366 private void retryFailedReceivedMessages(ReceiveMessageHandler handler
, boolean ignoreAttachments
) {
1367 final File cachePath
= new File(getMessageCachePath());
1368 if (!cachePath
.exists()) {
1371 for (final File dir
: Objects
.requireNonNull(cachePath
.listFiles())) {
1372 if (!dir
.isDirectory()) {
1373 retryFailedReceivedMessage(handler
, ignoreAttachments
, dir
);
1377 for (final File fileEntry
: Objects
.requireNonNull(dir
.listFiles())) {
1378 if (!fileEntry
.isFile()) {
1381 retryFailedReceivedMessage(handler
, ignoreAttachments
, fileEntry
);
1383 // Try to delete directory if empty
1388 private void retryFailedReceivedMessage(final ReceiveMessageHandler handler
, final boolean ignoreAttachments
, final File fileEntry
) {
1389 SignalServiceEnvelope envelope
;
1391 envelope
= Utils
.loadEnvelope(fileEntry
);
1392 if (envelope
== null) {
1395 } catch (IOException e
) {
1396 e
.printStackTrace();
1399 SignalServiceContent content
= null;
1400 if (!envelope
.isReceipt()) {
1402 content
= decryptMessage(envelope
);
1403 } catch (org
.whispersystems
.libsignal
.UntrustedIdentityException e
) {
1405 } catch (Exception er
) {
1406 // All other errors are not recoverable, so delete the cached message
1408 Files
.delete(fileEntry
.toPath());
1409 } catch (IOException e
) {
1410 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1414 List
<HandleAction
> actions
= handleMessage(envelope
, content
, ignoreAttachments
);
1415 for (HandleAction action
: actions
) {
1417 action
.execute(this);
1418 } catch (Throwable e
) {
1419 e
.printStackTrace();
1424 handler
.handleMessage(envelope
, content
, null);
1426 Files
.delete(fileEntry
.toPath());
1427 } catch (IOException e
) {
1428 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1432 public void receiveMessages(long timeout
, TimeUnit unit
, boolean returnOnTimeout
, boolean ignoreAttachments
, ReceiveMessageHandler handler
) throws IOException
{
1433 retryFailedReceivedMessages(handler
, ignoreAttachments
);
1434 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1436 Set
<HandleAction
> queuedActions
= null;
1438 if (messagePipe
== null) {
1439 messagePipe
= messageReceiver
.createMessagePipe();
1442 boolean hasCaughtUpWithOldMessages
= false;
1445 SignalServiceEnvelope envelope
;
1446 SignalServiceContent content
= null;
1447 Exception exception
= null;
1448 final long now
= new Date().getTime();
1450 Optional
<SignalServiceEnvelope
> result
= messagePipe
.readOrEmpty(timeout
, unit
, envelope1
-> {
1451 // store message on disk, before acknowledging receipt to the server
1453 String source
= envelope1
.getSourceE164().isPresent() ? envelope1
.getSourceE164().get() : "";
1454 File cacheFile
= getMessageCacheFile(source
, now
, envelope1
.getTimestamp());
1455 Utils
.storeEnvelope(envelope1
, cacheFile
);
1456 } catch (IOException e
) {
1457 System
.err
.println("Failed to store encrypted message in disk cache, ignoring: " + e
.getMessage());
1460 if (result
.isPresent()) {
1461 envelope
= result
.get();
1463 // Received indicator that server queue is empty
1464 hasCaughtUpWithOldMessages
= true;
1466 if (queuedActions
!= null) {
1467 for (HandleAction action
: queuedActions
) {
1469 action
.execute(this);
1470 } catch (Throwable e
) {
1471 e
.printStackTrace();
1475 queuedActions
.clear();
1476 queuedActions
= null;
1479 // Continue to wait another timeout for new messages
1482 } catch (TimeoutException e
) {
1483 if (returnOnTimeout
)
1486 } catch (InvalidVersionException e
) {
1487 System
.err
.println("Ignoring error: " + e
.getMessage());
1491 if (envelope
.hasSource()) {
1492 // Store uuid if we don't have it already
1493 SignalServiceAddress source
= envelope
.getSourceAddress();
1494 resolveSignalServiceAddress(source
);
1496 if (!envelope
.isReceipt()) {
1498 content
= decryptMessage(envelope
);
1499 } catch (Exception e
) {
1502 List
<HandleAction
> actions
= handleMessage(envelope
, content
, ignoreAttachments
);
1503 if (hasCaughtUpWithOldMessages
) {
1504 for (HandleAction action
: actions
) {
1506 action
.execute(this);
1507 } catch (Throwable e
) {
1508 e
.printStackTrace();
1512 if (queuedActions
== null) {
1513 queuedActions
= new HashSet
<>();
1515 queuedActions
.addAll(actions
);
1519 if (!isMessageBlocked(envelope
, content
)) {
1520 handler
.handleMessage(envelope
, content
, exception
);
1522 if (!(exception
instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
)) {
1523 File cacheFile
= null;
1525 String source
= envelope
.getSourceE164().isPresent() ? envelope
.getSourceE164().get() : "";
1526 cacheFile
= getMessageCacheFile(source
, now
, envelope
.getTimestamp());
1527 Files
.delete(cacheFile
.toPath());
1528 // Try to delete directory if empty
1529 new File(getMessageCachePath()).delete();
1530 } catch (IOException e
) {
1531 System
.err
.println("Failed to delete cached message file “" + cacheFile
+ "”: " + e
.getMessage());
1537 private boolean isMessageBlocked(SignalServiceEnvelope envelope
, SignalServiceContent content
) {
1538 SignalServiceAddress source
;
1539 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1540 source
= envelope
.getSourceAddress();
1541 } else if (content
!= null) {
1542 source
= content
.getSender();
1546 ContactInfo sourceContact
= account
.getContactStore().getContact(source
);
1547 if (sourceContact
!= null && sourceContact
.blocked
) {
1551 if (content
!= null && content
.getDataMessage().isPresent()) {
1552 SignalServiceDataMessage message
= content
.getDataMessage().get();
1553 if (message
.getGroupContext().isPresent() && message
.getGroupContext().get().getGroupV1().isPresent()) {
1554 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1555 GroupInfo group
= getGroup(groupInfo
.getGroupId());
1556 if (groupInfo
.getType() == SignalServiceGroup
.Type
.DELIVER
&& group
!= null && group
.blocked
) {
1564 private List
<HandleAction
> handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent content
, boolean ignoreAttachments
) {
1565 List
<HandleAction
> actions
= new ArrayList
<>();
1566 if (content
!= null) {
1567 SignalServiceAddress sender
;
1568 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1569 sender
= envelope
.getSourceAddress();
1571 sender
= content
.getSender();
1573 // Store uuid if we don't have it already
1574 resolveSignalServiceAddress(sender
);
1576 if (content
.getDataMessage().isPresent()) {
1577 SignalServiceDataMessage message
= content
.getDataMessage().get();
1579 if (content
.isNeedsReceipt()) {
1580 actions
.add(new SendReceiptAction(sender
, message
.getTimestamp()));
1583 actions
.addAll(handleSignalServiceDataMessage(message
, false, sender
, account
.getSelfAddress(), ignoreAttachments
));
1585 if (content
.getSyncMessage().isPresent()) {
1586 account
.setMultiDevice(true);
1587 SignalServiceSyncMessage syncMessage
= content
.getSyncMessage().get();
1588 if (syncMessage
.getSent().isPresent()) {
1589 SentTranscriptMessage message
= syncMessage
.getSent().get();
1590 actions
.addAll(handleSignalServiceDataMessage(message
.getMessage(), true, sender
, message
.getDestination().orNull(), ignoreAttachments
));
1592 if (syncMessage
.getRequest().isPresent()) {
1593 RequestMessage rm
= syncMessage
.getRequest().get();
1594 if (rm
.isContactsRequest()) {
1595 actions
.add(SendSyncContactsAction
.create());
1597 if (rm
.isGroupsRequest()) {
1598 actions
.add(SendSyncGroupsAction
.create());
1600 if (rm
.isBlockedListRequest()) {
1601 actions
.add(SendSyncBlockedListAction
.create());
1603 // TODO Handle rm.isConfigurationRequest();
1605 if (syncMessage
.getGroups().isPresent()) {
1606 File tmpFile
= null;
1608 tmpFile
= IOUtils
.createTempFile();
1609 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(syncMessage
.getGroups().get().asPointer(), tmpFile
)) {
1610 DeviceGroupsInputStream s
= new DeviceGroupsInputStream(attachmentAsStream
);
1612 while ((g
= s
.read()) != null) {
1613 GroupInfo syncGroup
= account
.getGroupStore().getGroup(g
.getId());
1614 if (syncGroup
== null) {
1615 syncGroup
= new GroupInfo(g
.getId());
1617 if (g
.getName().isPresent()) {
1618 syncGroup
.name
= g
.getName().get();
1620 syncGroup
.addMembers(g
.getMembers()
1622 .map(this::resolveSignalServiceAddress
)
1623 .collect(Collectors
.toSet()));
1624 if (!g
.isActive()) {
1625 syncGroup
.removeMember(account
.getSelfAddress());
1627 // Add ourself to the member set as it's marked as active
1628 syncGroup
.addMembers(Collections
.singleton(account
.getSelfAddress()));
1630 syncGroup
.blocked
= g
.isBlocked();
1631 if (g
.getColor().isPresent()) {
1632 syncGroup
.color
= g
.getColor().get();
1635 if (g
.getAvatar().isPresent()) {
1636 retrieveGroupAvatarAttachment(g
.getAvatar().get(), syncGroup
.groupId
);
1638 syncGroup
.inboxPosition
= g
.getInboxPosition().orNull();
1639 syncGroup
.archived
= g
.isArchived();
1640 account
.getGroupStore().updateGroup(syncGroup
);
1643 } catch (Exception e
) {
1644 e
.printStackTrace();
1646 if (tmpFile
!= null) {
1648 Files
.delete(tmpFile
.toPath());
1649 } catch (IOException e
) {
1650 System
.err
.println("Failed to delete received groups temp file “" + tmpFile
+ "”: " + e
.getMessage());
1655 if (syncMessage
.getBlockedList().isPresent()) {
1656 final BlockedListMessage blockedListMessage
= syncMessage
.getBlockedList().get();
1657 for (SignalServiceAddress address
: blockedListMessage
.getAddresses()) {
1658 setContactBlocked(resolveSignalServiceAddress(address
), true);
1660 for (byte[] groupId
: blockedListMessage
.getGroupIds()) {
1662 setGroupBlocked(groupId
, true);
1663 } catch (GroupNotFoundException e
) {
1664 System
.err
.println("BlockedListMessage contained groupID that was not found in GroupStore: " + Base64
.encodeBytes(groupId
));
1668 if (syncMessage
.getContacts().isPresent()) {
1669 File tmpFile
= null;
1671 tmpFile
= IOUtils
.createTempFile();
1672 final ContactsMessage contactsMessage
= syncMessage
.getContacts().get();
1673 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(contactsMessage
.getContactsStream().asPointer(), tmpFile
)) {
1674 DeviceContactsInputStream s
= new DeviceContactsInputStream(attachmentAsStream
);
1675 if (contactsMessage
.isComplete()) {
1676 account
.getContactStore().clear();
1679 while ((c
= s
.read()) != null) {
1680 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
1681 account
.setProfileKey(c
.getProfileKey().get());
1683 final SignalServiceAddress address
= resolveSignalServiceAddress(c
.getAddress());
1684 ContactInfo contact
= account
.getContactStore().getContact(address
);
1685 if (contact
== null) {
1686 contact
= new ContactInfo(address
);
1688 if (c
.getName().isPresent()) {
1689 contact
.name
= c
.getName().get();
1691 if (c
.getColor().isPresent()) {
1692 contact
.color
= c
.getColor().get();
1694 if (c
.getProfileKey().isPresent()) {
1695 account
.getProfileStore().storeProfileKey(address
, c
.getProfileKey().get());
1697 if (c
.getVerified().isPresent()) {
1698 final VerifiedMessage verifiedMessage
= c
.getVerified().get();
1699 account
.getSignalProtocolStore().setIdentityTrustLevel(verifiedMessage
.getDestination(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1701 if (c
.getExpirationTimer().isPresent()) {
1702 contact
.messageExpirationTime
= c
.getExpirationTimer().get();
1704 contact
.blocked
= c
.isBlocked();
1705 contact
.inboxPosition
= c
.getInboxPosition().orNull();
1706 contact
.archived
= c
.isArchived();
1707 account
.getContactStore().updateContact(contact
);
1709 if (c
.getAvatar().isPresent()) {
1710 retrieveContactAvatarAttachment(c
.getAvatar().get(), contact
.number
);
1714 } catch (Exception e
) {
1715 e
.printStackTrace();
1717 if (tmpFile
!= null) {
1719 Files
.delete(tmpFile
.toPath());
1720 } catch (IOException e
) {
1721 System
.err
.println("Failed to delete received contacts temp file “" + tmpFile
+ "”: " + e
.getMessage());
1726 if (syncMessage
.getVerified().isPresent()) {
1727 final VerifiedMessage verifiedMessage
= syncMessage
.getVerified().get();
1728 account
.getSignalProtocolStore().setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage
.getDestination()), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1730 if (syncMessage
.getConfiguration().isPresent()) {
1738 private File
getContactAvatarFile(String number
) {
1739 return new File(pathConfig
.getAvatarsPath(), "contact-" + number
);
1742 private File
retrieveContactAvatarAttachment(SignalServiceAttachment attachment
, String number
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1743 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
1744 if (attachment
.isPointer()) {
1745 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1746 return retrieveAttachment(pointer
, getContactAvatarFile(number
), false);
1748 SignalServiceAttachmentStream stream
= attachment
.asStream();
1749 return Utils
.retrieveAttachment(stream
, getContactAvatarFile(number
));
1753 private File
getGroupAvatarFile(byte[] groupId
) {
1754 return new File(pathConfig
.getAvatarsPath(), "group-" + Base64
.encodeBytes(groupId
).replace("/", "_"));
1757 private File
retrieveGroupAvatarAttachment(SignalServiceAttachment attachment
, byte[] groupId
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1758 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
1759 if (attachment
.isPointer()) {
1760 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1761 return retrieveAttachment(pointer
, getGroupAvatarFile(groupId
), false);
1763 SignalServiceAttachmentStream stream
= attachment
.asStream();
1764 return Utils
.retrieveAttachment(stream
, getGroupAvatarFile(groupId
));
1768 private File
getProfileAvatarFile(SignalServiceAddress address
) {
1769 return new File(pathConfig
.getAvatarsPath(), "profile-" + address
.getLegacyIdentifier());
1772 private File
retrieveProfileAvatar(SignalServiceAddress address
, String avatarPath
, ProfileKey profileKey
) throws IOException
{
1773 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
1774 SignalServiceMessageReceiver receiver
= getMessageReceiver();
1775 File outputFile
= getProfileAvatarFile(address
);
1777 File tmpFile
= IOUtils
.createTempFile();
1778 try (InputStream input
= receiver
.retrieveProfileAvatar(avatarPath
, tmpFile
, profileKey
, ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
)) {
1779 // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
1780 IOUtils
.copyStreamToFile(input
, outputFile
, (int) ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
);
1783 Files
.delete(tmpFile
.toPath());
1784 } catch (IOException e
) {
1785 System
.err
.println("Failed to delete received avatar temp file “" + tmpFile
+ "”: " + e
.getMessage());
1791 public File
getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId
) {
1792 return new File(pathConfig
.getAttachmentsPath(), attachmentId
.toString());
1795 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1796 IOUtils
.createPrivateDirectories(pathConfig
.getAttachmentsPath());
1797 return retrieveAttachment(pointer
, getAttachmentFile(pointer
.getRemoteId()), true);
1800 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
, File outputFile
, boolean storePreview
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1801 if (storePreview
&& pointer
.getPreview().isPresent()) {
1802 File previewFile
= new File(outputFile
+ ".preview");
1803 try (OutputStream output
= new FileOutputStream(previewFile
)) {
1804 byte[] preview
= pointer
.getPreview().get();
1805 output
.write(preview
, 0, preview
.length
);
1806 } catch (FileNotFoundException e
) {
1807 e
.printStackTrace();
1812 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1814 File tmpFile
= IOUtils
.createTempFile();
1815 try (InputStream input
= messageReceiver
.retrieveAttachment(pointer
, tmpFile
, ServiceConfig
.MAX_ATTACHMENT_SIZE
)) {
1816 IOUtils
.copyStreamToFile(input
, outputFile
);
1819 Files
.delete(tmpFile
.toPath());
1820 } catch (IOException e
) {
1821 System
.err
.println("Failed to delete received attachment temp file “" + tmpFile
+ "”: " + e
.getMessage());
1827 private InputStream
retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer
, File tmpFile
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1828 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1829 return messageReceiver
.retrieveAttachment(pointer
, tmpFile
, ServiceConfig
.MAX_ATTACHMENT_SIZE
);
1832 void sendGroups() throws IOException
, UntrustedIdentityException
{
1833 File groupsFile
= IOUtils
.createTempFile();
1836 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
1837 DeviceGroupsOutputStream out
= new DeviceGroupsOutputStream(fos
);
1838 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1839 out
.write(new DeviceGroup(record.groupId
, Optional
.fromNullable(record.name
),
1840 new ArrayList
<>(record.getMembers()), createGroupAvatarAttachment(record.groupId
),
1841 record.isMember(account
.getSelfAddress()), Optional
.of(record.messageExpirationTime
),
1842 Optional
.fromNullable(record.color
), record.blocked
, Optional
.fromNullable(record.inboxPosition
), record.archived
));
1846 if (groupsFile
.exists() && groupsFile
.length() > 0) {
1847 try (FileInputStream groupsFileStream
= new FileInputStream(groupsFile
)) {
1848 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1849 .withStream(groupsFileStream
)
1850 .withContentType("application/octet-stream")
1851 .withLength(groupsFile
.length())
1854 sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
1859 Files
.delete(groupsFile
.toPath());
1860 } catch (IOException e
) {
1861 System
.err
.println("Failed to delete groups temp file “" + groupsFile
+ "”: " + e
.getMessage());
1866 public void sendContacts() throws IOException
, UntrustedIdentityException
{
1867 File contactsFile
= IOUtils
.createTempFile();
1870 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
1871 DeviceContactsOutputStream out
= new DeviceContactsOutputStream(fos
);
1872 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1873 VerifiedMessage verifiedMessage
= null;
1874 JsonIdentityKeyStore
.Identity currentIdentity
= account
.getSignalProtocolStore().getIdentity(record.getAddress());
1875 if (currentIdentity
!= null) {
1876 verifiedMessage
= new VerifiedMessage(record.getAddress(), currentIdentity
.getIdentityKey(), currentIdentity
.getTrustLevel().toVerifiedState(), currentIdentity
.getDateAdded().getTime());
1879 ProfileKey profileKey
= account
.getProfileStore().getProfileKey(record.getAddress());
1880 out
.write(new DeviceContact(record.getAddress(), Optional
.fromNullable(record.name
),
1881 createContactAvatarAttachment(record.number
), Optional
.fromNullable(record.color
),
1882 Optional
.fromNullable(verifiedMessage
), Optional
.fromNullable(profileKey
), record.blocked
,
1883 Optional
.of(record.messageExpirationTime
),
1884 Optional
.fromNullable(record.inboxPosition
), record.archived
));
1887 if (account
.getProfileKey() != null) {
1888 // Send our own profile key as well
1889 out
.write(new DeviceContact(account
.getSelfAddress(),
1890 Optional
.absent(), Optional
.absent(),
1891 Optional
.absent(), Optional
.absent(),
1892 Optional
.of(account
.getProfileKey()),
1893 false, Optional
.absent(), Optional
.absent(), false));
1897 if (contactsFile
.exists() && contactsFile
.length() > 0) {
1898 try (FileInputStream contactsFileStream
= new FileInputStream(contactsFile
)) {
1899 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1900 .withStream(contactsFileStream
)
1901 .withContentType("application/octet-stream")
1902 .withLength(contactsFile
.length())
1905 sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
, true)));
1910 Files
.delete(contactsFile
.toPath());
1911 } catch (IOException e
) {
1912 System
.err
.println("Failed to delete contacts temp file “" + contactsFile
+ "”: " + e
.getMessage());
1917 void sendBlockedList() throws IOException
, UntrustedIdentityException
{
1918 List
<SignalServiceAddress
> addresses
= new ArrayList
<>();
1919 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1920 if (record.blocked
) {
1921 addresses
.add(record.getAddress());
1924 List
<byte[]> groupIds
= new ArrayList
<>();
1925 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1926 if (record.blocked
) {
1927 groupIds
.add(record.groupId
);
1930 sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
1933 private void sendVerifiedMessage(SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
) throws IOException
, UntrustedIdentityException
{
1934 VerifiedMessage verifiedMessage
= new VerifiedMessage(destination
, identityKey
, trustLevel
.toVerifiedState(), System
.currentTimeMillis());
1935 sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
1938 public List
<ContactInfo
> getContacts() {
1939 return account
.getContactStore().getContacts();
1942 public ContactInfo
getContact(String number
) {
1943 return account
.getContactStore().getContact(Util
.getSignalServiceAddressFromIdentifier(number
));
1946 public GroupInfo
getGroup(byte[] groupId
) {
1947 return account
.getGroupStore().getGroup(groupId
);
1950 public List
<JsonIdentityKeyStore
.Identity
> getIdentities() {
1951 return account
.getSignalProtocolStore().getIdentities();
1954 public List
<JsonIdentityKeyStore
.Identity
> getIdentities(String number
) throws InvalidNumberException
{
1955 return account
.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number
));
1959 * Trust this the identity with this fingerprint
1961 * @param name username of the identity
1962 * @param fingerprint Fingerprint
1964 public boolean trustIdentityVerified(String name
, byte[] fingerprint
) throws InvalidNumberException
{
1965 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
1966 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
1970 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1971 if (!Arrays
.equals(id
.getIdentityKey().serialize(), fingerprint
)) {
1975 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1977 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1978 } catch (IOException
| UntrustedIdentityException e
) {
1979 e
.printStackTrace();
1988 * Trust this the identity with this safety number
1990 * @param name username of the identity
1991 * @param safetyNumber Safety number
1993 public boolean trustIdentityVerifiedSafetyNumber(String name
, String safetyNumber
) throws InvalidNumberException
{
1994 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
1995 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
1999 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2000 if (!safetyNumber
.equals(computeSafetyNumber(address
, id
.getIdentityKey()))) {
2004 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2006 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2007 } catch (IOException
| UntrustedIdentityException e
) {
2008 e
.printStackTrace();
2017 * Trust all keys of this identity without verification
2019 * @param name username of the identity
2021 public boolean trustIdentityAllKeys(String name
) {
2022 SignalServiceAddress address
= resolveSignalServiceAddress(name
);
2023 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2027 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2028 if (id
.getTrustLevel() == TrustLevel
.UNTRUSTED
) {
2029 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2031 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2032 } catch (IOException
| UntrustedIdentityException e
) {
2033 e
.printStackTrace();
2041 public String
computeSafetyNumber(SignalServiceAddress theirAddress
, IdentityKey theirIdentityKey
) {
2042 return Utils
.computeSafetyNumber(account
.getSelfAddress(), getIdentityKeyPair().getPublicKey(), theirAddress
, theirIdentityKey
);
2045 void saveAccount() {
2049 public SignalServiceAddress
canonicalizeAndResolveSignalServiceAddress(String identifier
) throws InvalidNumberException
{
2050 String canonicalizedNumber
= UuidUtil
.isUuid(identifier
) ? identifier
: Util
.canonicalizeNumber(identifier
, account
.getUsername());
2051 return resolveSignalServiceAddress(canonicalizedNumber
);
2054 public SignalServiceAddress
resolveSignalServiceAddress(String identifier
) {
2055 SignalServiceAddress address
= Util
.getSignalServiceAddressFromIdentifier(identifier
);
2057 return resolveSignalServiceAddress(address
);
2060 public SignalServiceAddress
resolveSignalServiceAddress(SignalServiceAddress address
) {
2061 if (address
.matches(account
.getSelfAddress())) {
2062 return account
.getSelfAddress();
2065 return account
.getRecipientStore().resolveServiceAddress(address
);
2069 public void close() throws IOException
{
2070 if (messagePipe
!= null) {
2071 messagePipe
.shutdown();
2075 if (unidentifiedMessagePipe
!= null) {
2076 unidentifiedMessagePipe
.shutdown();
2077 unidentifiedMessagePipe
= null;
2083 public interface ReceiveMessageHandler
{
2085 void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent decryptedContent
, Throwable e
);