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
.messages
.SendMessageResult
;
70 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachment
;
71 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentPointer
;
72 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentRemoteId
;
73 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentStream
;
74 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceContent
;
75 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceDataMessage
;
76 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceEnvelope
;
77 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroup
;
78 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceReceiptMessage
;
79 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceStickerManifestUpload
;
80 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceStickerManifestUpload
.StickerInfo
;
81 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.BlockedListMessage
;
82 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ContactsMessage
;
83 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContact
;
84 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsInputStream
;
85 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsOutputStream
;
86 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroup
;
87 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsInputStream
;
88 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsOutputStream
;
89 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceInfo
;
90 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.RequestMessage
;
91 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SentTranscriptMessage
;
92 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SignalServiceSyncMessage
;
93 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.VerifiedMessage
;
94 import org
.whispersystems
.signalservice
.api
.profiles
.SignalServiceProfile
;
95 import org
.whispersystems
.signalservice
.api
.push
.ContactTokenDetails
;
96 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
97 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.EncapsulatedExceptions
;
98 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.MissingConfigurationException
;
99 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NetworkFailureException
;
100 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.UnregisteredUserException
;
101 import org
.whispersystems
.signalservice
.api
.util
.InvalidNumberException
;
102 import org
.whispersystems
.signalservice
.api
.util
.SleepTimer
;
103 import org
.whispersystems
.signalservice
.api
.util
.StreamDetails
;
104 import org
.whispersystems
.signalservice
.api
.util
.UptimeSleepTimer
;
105 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
106 import org
.whispersystems
.signalservice
.internal
.configuration
.SignalServiceConfiguration
;
107 import org
.whispersystems
.signalservice
.internal
.push
.SignalServiceProtos
;
108 import org
.whispersystems
.signalservice
.internal
.push
.UnsupportedDataMessageException
;
109 import org
.whispersystems
.signalservice
.internal
.push
.VerifyAccountResponse
;
110 import org
.whispersystems
.signalservice
.internal
.util
.Hex
;
111 import org
.whispersystems
.util
.Base64
;
113 import java
.io
.Closeable
;
115 import java
.io
.FileInputStream
;
116 import java
.io
.FileNotFoundException
;
117 import java
.io
.FileOutputStream
;
118 import java
.io
.IOException
;
119 import java
.io
.InputStream
;
120 import java
.io
.OutputStream
;
122 import java
.net
.URISyntaxException
;
123 import java
.net
.URLEncoder
;
124 import java
.nio
.file
.Files
;
125 import java
.nio
.file
.Paths
;
126 import java
.nio
.file
.StandardCopyOption
;
127 import java
.util
.ArrayList
;
128 import java
.util
.Arrays
;
129 import java
.util
.Collection
;
130 import java
.util
.Collections
;
131 import java
.util
.Date
;
132 import java
.util
.HashSet
;
133 import java
.util
.LinkedList
;
134 import java
.util
.List
;
135 import java
.util
.Locale
;
136 import java
.util
.Objects
;
137 import java
.util
.Set
;
138 import java
.util
.UUID
;
139 import java
.util
.concurrent
.ExecutionException
;
140 import java
.util
.concurrent
.ExecutorService
;
141 import java
.util
.concurrent
.TimeUnit
;
142 import java
.util
.concurrent
.TimeoutException
;
143 import java
.util
.stream
.Collectors
;
144 import java
.util
.zip
.ZipEntry
;
145 import java
.util
.zip
.ZipFile
;
147 import static org
.asamk
.signal
.manager
.ServiceConfig
.capabilities
;
149 public class Manager
implements Closeable
{
151 private final SleepTimer timer
= new UptimeSleepTimer();
152 private final SignalServiceConfiguration serviceConfiguration
;
153 private final String userAgent
;
155 private final SignalAccount account
;
156 private final PathConfig pathConfig
;
157 private SignalServiceAccountManager accountManager
;
158 private SignalServiceMessagePipe messagePipe
= null;
159 private SignalServiceMessagePipe unidentifiedMessagePipe
= null;
160 private boolean discoverableByPhoneNumber
= true;
162 public Manager(SignalAccount account
, PathConfig pathConfig
, SignalServiceConfiguration serviceConfiguration
, String userAgent
) {
163 this.account
= account
;
164 this.pathConfig
= pathConfig
;
165 this.serviceConfiguration
= serviceConfiguration
;
166 this.userAgent
= userAgent
;
167 this.accountManager
= createSignalServiceAccountManager();
169 this.account
.setResolver(this::resolveSignalServiceAddress
);
172 public String
getUsername() {
173 return account
.getUsername();
176 public SignalServiceAddress
getSelfAddress() {
177 return account
.getSelfAddress();
180 private SignalServiceAccountManager
createSignalServiceAccountManager() {
181 return new SignalServiceAccountManager(serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), userAgent
, timer
);
184 private IdentityKeyPair
getIdentityKeyPair() {
185 return account
.getSignalProtocolStore().getIdentityKeyPair();
188 public int getDeviceId() {
189 return account
.getDeviceId();
192 private String
getMessageCachePath() {
193 return pathConfig
.getDataPath() + "/" + account
.getUsername() + ".d/msg-cache";
196 private String
getMessageCachePath(String sender
) {
197 if (sender
== null || sender
.isEmpty()) {
198 return getMessageCachePath();
201 return getMessageCachePath() + "/" + sender
.replace("/", "_");
204 private File
getMessageCacheFile(String sender
, long now
, long timestamp
) throws IOException
{
205 String cachePath
= getMessageCachePath(sender
);
206 IOUtils
.createPrivateDirectories(cachePath
);
207 return new File(cachePath
+ "/" + now
+ "_" + timestamp
);
210 public static Manager
init(String username
, String settingsPath
, SignalServiceConfiguration serviceConfiguration
, String userAgent
) throws IOException
{
211 PathConfig pathConfig
= PathConfig
.createDefault(settingsPath
);
213 if (!SignalAccount
.userExists(pathConfig
.getDataPath(), username
)) {
214 IdentityKeyPair identityKey
= KeyHelper
.generateIdentityKeyPair();
215 int registrationId
= KeyHelper
.generateRegistrationId(false);
217 ProfileKey profileKey
= KeyUtils
.createProfileKey();
218 SignalAccount account
= SignalAccount
.create(pathConfig
.getDataPath(), username
, identityKey
, registrationId
, profileKey
);
221 return new Manager(account
, pathConfig
, serviceConfiguration
, userAgent
);
224 SignalAccount account
= SignalAccount
.load(pathConfig
.getDataPath(), username
);
226 Manager m
= new Manager(account
, pathConfig
, serviceConfiguration
, userAgent
);
228 m
.migrateLegacyConfigs();
233 private void migrateLegacyConfigs() {
234 // Copy group avatars that were previously stored in the attachments folder
235 // to the new avatar folder
236 if (JsonGroupStore
.groupsWithLegacyAvatarId
.size() > 0) {
237 for (GroupInfo g
: JsonGroupStore
.groupsWithLegacyAvatarId
) {
238 File avatarFile
= getGroupAvatarFile(g
.groupId
);
239 File attachmentFile
= getAttachmentFile(new SignalServiceAttachmentRemoteId(g
.getAvatarId()));
240 if (!avatarFile
.exists() && attachmentFile
.exists()) {
242 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
243 Files
.copy(attachmentFile
.toPath(), avatarFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
244 } catch (Exception e
) {
249 JsonGroupStore
.groupsWithLegacyAvatarId
.clear();
252 if (account
.getProfileKey() == null) {
253 // Old config file, creating new profile key
254 account
.setProfileKey(KeyUtils
.createProfileKey());
259 public void checkAccountState() throws IOException
{
260 if (account
.isRegistered()) {
261 if (accountManager
.getPreKeysCount() < ServiceConfig
.PREKEY_MINIMUM_COUNT
) {
265 if (account
.getUuid() == null) {
266 account
.setUuid(accountManager
.getOwnUuid());
272 public boolean isRegistered() {
273 return account
.isRegistered();
276 public void register(boolean voiceVerification
) throws IOException
{
277 account
.setPassword(KeyUtils
.createPassword());
279 // Resetting UUID, because registering doesn't work otherwise
280 account
.setUuid(null);
281 accountManager
= createSignalServiceAccountManager();
283 if (voiceVerification
) {
284 accountManager
.requestVoiceVerificationCode(Locale
.getDefault(), Optional
.absent(), Optional
.absent());
286 accountManager
.requestSmsVerificationCode(false, Optional
.absent(), Optional
.absent());
289 account
.setRegistered(false);
293 public void updateAccountAttributes() throws IOException
{
294 accountManager
.setAccountAttributes(account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, account
.getRegistrationLockPin(), account
.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities
, discoverableByPhoneNumber
);
297 public void setProfile(String name
, File avatar
) throws IOException
{
298 try (final StreamDetails streamDetails
= avatar
== null ?
null : Utils
.createStreamDetailsFromFile(avatar
)) {
299 accountManager
.setVersionedProfile(account
.getUuid(), account
.getProfileKey(), name
, streamDetails
);
303 public void unregister() throws IOException
{
304 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
305 // If this is the master device, other users can't send messages to this number anymore.
306 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
307 accountManager
.setGcmId(Optional
.absent());
309 account
.setRegistered(false);
313 public List
<DeviceInfo
> getLinkedDevices() throws IOException
{
314 List
<DeviceInfo
> devices
= accountManager
.getDevices();
315 account
.setMultiDevice(devices
.size() > 1);
320 public void removeLinkedDevices(int deviceId
) throws IOException
{
321 accountManager
.removeDevice(deviceId
);
322 List
<DeviceInfo
> devices
= accountManager
.getDevices();
323 account
.setMultiDevice(devices
.size() > 1);
327 public void addDeviceLink(URI linkUri
) throws IOException
, InvalidKeyException
{
328 Utils
.DeviceLinkInfo info
= Utils
.parseDeviceLinkUri(linkUri
);
330 addDevice(info
.deviceIdentifier
, info
.deviceKey
);
333 private void addDevice(String deviceIdentifier
, ECPublicKey deviceKey
) throws IOException
, InvalidKeyException
{
334 IdentityKeyPair identityKeyPair
= getIdentityKeyPair();
335 String verificationCode
= accountManager
.getNewDeviceVerificationCode();
337 accountManager
.addDevice(deviceIdentifier
, deviceKey
, identityKeyPair
, Optional
.of(account
.getProfileKey().serialize()), verificationCode
);
338 account
.setMultiDevice(true);
342 private List
<PreKeyRecord
> generatePreKeys() {
343 List
<PreKeyRecord
> records
= new ArrayList
<>(ServiceConfig
.PREKEY_BATCH_SIZE
);
345 final int offset
= account
.getPreKeyIdOffset();
346 for (int i
= 0; i
< ServiceConfig
.PREKEY_BATCH_SIZE
; i
++) {
347 int preKeyId
= (offset
+ i
) % Medium
.MAX_VALUE
;
348 ECKeyPair keyPair
= Curve
.generateKeyPair();
349 PreKeyRecord
record = new PreKeyRecord(preKeyId
, keyPair
);
354 account
.addPreKeys(records
);
360 private SignedPreKeyRecord
generateSignedPreKey(IdentityKeyPair identityKeyPair
) {
362 ECKeyPair keyPair
= Curve
.generateKeyPair();
363 byte[] signature
= Curve
.calculateSignature(identityKeyPair
.getPrivateKey(), keyPair
.getPublicKey().serialize());
364 SignedPreKeyRecord
record = new SignedPreKeyRecord(account
.getNextSignedPreKeyId(), System
.currentTimeMillis(), keyPair
, signature
);
366 account
.addSignedPreKey(record);
370 } catch (InvalidKeyException e
) {
371 throw new AssertionError(e
);
375 public void verifyAccount(String verificationCode
, String pin
) throws IOException
{
376 verificationCode
= verificationCode
.replace("-", "");
377 account
.setSignalingKey(KeyUtils
.createSignalingKey());
378 // TODO make unrestricted unidentified access configurable
379 VerifyAccountResponse response
= accountManager
.verifyAccountWithCode(verificationCode
, account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, pin
, null, getSelfUnidentifiedAccessKey(), false, capabilities
, discoverableByPhoneNumber
);
381 UUID uuid
= UuidUtil
.parseOrNull(response
.getUuid());
382 // TODO response.isStorageCapable()
383 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
384 account
.setRegistered(true);
385 account
.setUuid(uuid
);
386 account
.setRegistrationLockPin(pin
);
387 account
.getSignalProtocolStore().saveIdentity(account
.getSelfAddress(), getIdentityKeyPair().getPublicKey(), TrustLevel
.TRUSTED_VERIFIED
);
393 public void setRegistrationLockPin(Optional
<String
> pin
) throws IOException
{
394 if (pin
.isPresent()) {
395 account
.setRegistrationLockPin(pin
.get());
396 throw new RuntimeException("Not implemented anymore, will be replaced with KBS");
398 account
.setRegistrationLockPin(null);
399 accountManager
.removeRegistrationLockV1();
404 void refreshPreKeys() throws IOException
{
405 List
<PreKeyRecord
> oneTimePreKeys
= generatePreKeys();
406 final IdentityKeyPair identityKeyPair
= getIdentityKeyPair();
407 SignedPreKeyRecord signedPreKeyRecord
= generateSignedPreKey(identityKeyPair
);
409 accountManager
.setPreKeys(identityKeyPair
.getPublicKey(), signedPreKeyRecord
, oneTimePreKeys
);
412 private SignalServiceMessageReceiver
getMessageReceiver() {
413 // TODO implement ZkGroup support
414 final ClientZkProfileOperations clientZkProfileOperations
= null;
415 return new SignalServiceMessageReceiver(serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), account
.getSignalingKey(), userAgent
, null, timer
, clientZkProfileOperations
);
418 private SignalServiceMessageSender
getMessageSender() {
419 // TODO implement ZkGroup support
420 final ClientZkProfileOperations clientZkProfileOperations
= null;
421 final boolean attachmentsV3
= false;
422 final ExecutorService executor
= null;
423 return new SignalServiceMessageSender(serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(),
424 account
.getDeviceId(), account
.getSignalProtocolStore(), userAgent
, account
.isMultiDevice(), attachmentsV3
, Optional
.fromNullable(messagePipe
), Optional
.fromNullable(unidentifiedMessagePipe
), Optional
.absent(), clientZkProfileOperations
, executor
);
427 private SignalServiceProfile
getEncryptedRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
) throws IOException
{
428 SignalServiceMessagePipe pipe
= unidentifiedMessagePipe
!= null && unidentifiedAccess
.isPresent() ? unidentifiedMessagePipe
433 return pipe
.getProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).get(10, TimeUnit
.SECONDS
).getProfile();
434 } catch (IOException
| InterruptedException
| ExecutionException
| TimeoutException ignored
) {
438 SignalServiceMessageReceiver receiver
= getMessageReceiver();
440 return receiver
.retrieveProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).get(10, TimeUnit
.SECONDS
).getProfile();
441 } catch (InterruptedException
| ExecutionException
| TimeoutException e
) {
442 throw new IOException("Failed to retrieve profile", e
);
446 private SignalProfile
getRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
, ProfileKey profileKey
) throws IOException
{
447 SignalProfileEntry profileEntry
= account
.getProfileStore().getProfile(address
);
448 long now
= new Date().getTime();
449 // Profiles are cache for 24h before retrieving them again
450 if (profileEntry
== null || profileEntry
.getProfile() == null || now
- profileEntry
.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000) {
451 SignalProfile profile
= retrieveRecipientProfile(address
, unidentifiedAccess
, profileKey
);
452 profileEntry
= new SignalProfileEntry(profileKey
, now
, profile
);
453 account
.getProfileStore().updateProfile(address
, profileEntry
);
455 return profileEntry
.getProfile();
458 private SignalProfile
retrieveRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
, ProfileKey profileKey
) throws IOException
{
459 final SignalServiceProfile encryptedProfile
= getEncryptedRecipientProfile(address
, unidentifiedAccess
);
461 File avatarFile
= null;
463 avatarFile
= encryptedProfile
.getAvatar() == null ?
null : retrieveProfileAvatar(address
, encryptedProfile
.getAvatar(), profileKey
);
464 } catch (AssertionError e
) {
465 System
.err
.println("Failed to retrieve profile avatar: " + e
.getMessage());
468 ProfileCipher profileCipher
= new ProfileCipher(profileKey
);
470 return new SignalProfile(
471 encryptedProfile
.getIdentityKey(),
472 encryptedProfile
.getName() == null ?
null : new String(profileCipher
.decryptName(Base64
.decode(encryptedProfile
.getName()))),
474 encryptedProfile
.getUnidentifiedAccess() == null || !profileCipher
.verifyUnidentifiedAccess(Base64
.decode(encryptedProfile
.getUnidentifiedAccess())) ?
null : encryptedProfile
.getUnidentifiedAccess(),
475 encryptedProfile
.isUnrestrictedUnidentifiedAccess(),
476 encryptedProfile
.getCapabilities());
477 } catch (InvalidCiphertextException e
) {
482 private Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(byte[] groupId
) throws IOException
{
483 File file
= getGroupAvatarFile(groupId
);
484 if (!file
.exists()) {
485 return Optional
.absent();
488 return Optional
.of(Utils
.createAttachment(file
));
491 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(String number
) throws IOException
{
492 File file
= getContactAvatarFile(number
);
493 if (!file
.exists()) {
494 return Optional
.absent();
497 return Optional
.of(Utils
.createAttachment(file
));
500 private GroupInfo
getGroupForSending(byte[] groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
501 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
503 throw new GroupNotFoundException(groupId
);
505 if (!g
.isMember(account
.getSelfAddress())) {
506 throw new NotAGroupMemberException(groupId
, g
.name
);
511 public List
<GroupInfo
> getGroups() {
512 return account
.getGroupStore().getGroups();
515 public long sendGroupMessage(String messageText
, List
<String
> attachments
,
517 throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
, NotAGroupMemberException
{
518 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
519 if (attachments
!= null) {
520 messageBuilder
.withAttachments(Utils
.getSignalServiceAttachments(attachments
));
522 if (groupId
!= null) {
523 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
526 messageBuilder
.asGroupMessage(group
);
529 final GroupInfo g
= getGroupForSending(groupId
);
531 messageBuilder
.withExpiration(g
.messageExpirationTime
);
533 return sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
536 public void sendGroupMessageReaction(String emoji
, boolean remove
, String targetAuthor
,
537 long targetSentTimestamp
, byte[] groupId
)
538 throws IOException
, EncapsulatedExceptions
, InvalidNumberException
, NotAGroupMemberException
, GroupNotFoundException
{
539 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, canonicalizeAndResolveSignalServiceAddress(targetAuthor
), targetSentTimestamp
);
540 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
541 .withReaction(reaction
);
542 if (groupId
!= null) {
543 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
546 messageBuilder
.asGroupMessage(group
);
548 final GroupInfo g
= getGroupForSending(groupId
);
549 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
552 public void sendQuitGroupMessage(byte[] groupId
) throws GroupNotFoundException
, IOException
, EncapsulatedExceptions
, NotAGroupMemberException
{
553 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
557 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
558 .asGroupMessage(group
);
560 final GroupInfo g
= getGroupForSending(groupId
);
561 g
.removeMember(account
.getSelfAddress());
562 account
.getGroupStore().updateGroup(g
);
564 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
567 private byte[] sendUpdateGroupMessage(byte[] groupId
, String name
, Collection
<SignalServiceAddress
> members
, String avatarFile
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
, NotAGroupMemberException
{
569 if (groupId
== null) {
571 g
= new GroupInfo(KeyUtils
.createGroupId());
572 g
.addMembers(Collections
.singleton(account
.getSelfAddress()));
574 g
= getGroupForSending(groupId
);
581 if (members
!= null) {
582 final Set
<String
> newE164Members
= new HashSet
<>();
583 for (SignalServiceAddress member
: members
) {
584 if (g
.isMember(member
) || !member
.getNumber().isPresent()) {
587 newE164Members
.add(member
.getNumber().get());
590 final List
<ContactTokenDetails
> contacts
= accountManager
.getContacts(newE164Members
);
591 if (contacts
.size() != newE164Members
.size()) {
592 // Some of the new members are not registered on Signal
593 for (ContactTokenDetails contact
: contacts
) {
594 newE164Members
.remove(contact
.getNumber());
596 throw new IOException("Failed to add members " + Util
.join(", ", newE164Members
) + " to group: Not registered on Signal");
599 g
.addMembers(members
);
602 if (avatarFile
!= null) {
603 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
604 File aFile
= getGroupAvatarFile(g
.groupId
);
605 Files
.copy(Paths
.get(avatarFile
), aFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
608 account
.getGroupStore().updateGroup(g
);
610 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
612 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
616 void sendUpdateGroupMessage(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
, NotAGroupMemberException
, GroupNotFoundException
, AttachmentInvalidException
{
617 if (groupId
== null) {
620 GroupInfo g
= getGroupForSending(groupId
);
622 if (!g
.isMember(recipient
)) {
626 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
628 // Send group message only to the recipient who requested it
629 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
632 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfo g
) throws AttachmentInvalidException
{
633 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
636 .withMembers(new ArrayList
<>(g
.getMembers()));
638 File aFile
= getGroupAvatarFile(g
.groupId
);
639 if (aFile
.exists()) {
641 group
.withAvatar(Utils
.createAttachment(aFile
));
642 } catch (IOException e
) {
643 throw new AttachmentInvalidException(aFile
.toString(), e
);
647 return SignalServiceDataMessage
.newBuilder()
648 .asGroupMessage(group
.build())
649 .withExpiration(g
.messageExpirationTime
);
652 void sendGroupInfoRequest(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
653 if (groupId
== null) {
657 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
)
660 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
661 .asGroupMessage(group
.build());
663 // Send group info request message to the recipient who sent us a message with this groupId
664 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
667 void sendReceipt(SignalServiceAddress remoteAddress
, long messageId
) throws IOException
, UntrustedIdentityException
{
668 SignalServiceReceiptMessage receiptMessage
= new SignalServiceReceiptMessage(SignalServiceReceiptMessage
.Type
.DELIVERY
,
669 Collections
.singletonList(messageId
),
670 System
.currentTimeMillis());
672 getMessageSender().sendReceipt(remoteAddress
, getAccessFor(remoteAddress
), receiptMessage
);
675 public long sendMessage(String messageText
, List
<String
> attachments
,
676 List
<String
> recipients
)
677 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
678 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
679 if (attachments
!= null) {
680 List
<SignalServiceAttachment
> attachmentStreams
= Utils
.getSignalServiceAttachments(attachments
);
682 // Upload attachments here, so we only upload once even for multiple recipients
683 SignalServiceMessageSender messageSender
= getMessageSender();
684 List
<SignalServiceAttachment
> attachmentPointers
= new ArrayList
<>(attachmentStreams
.size());
685 for (SignalServiceAttachment attachment
: attachmentStreams
) {
686 if (attachment
.isStream()) {
687 attachmentPointers
.add(messageSender
.uploadAttachment(attachment
.asStream()));
688 } else if (attachment
.isPointer()) {
689 attachmentPointers
.add(attachment
.asPointer());
693 messageBuilder
.withAttachments(attachmentPointers
);
695 return sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
698 public void sendMessageReaction(String emoji
, boolean remove
, String targetAuthor
,
699 long targetSentTimestamp
, List
<String
> recipients
)
700 throws IOException
, EncapsulatedExceptions
, InvalidNumberException
{
701 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, canonicalizeAndResolveSignalServiceAddress(targetAuthor
), targetSentTimestamp
);
702 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
703 .withReaction(reaction
);
704 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
707 public void sendEndSessionMessage(List
<String
> recipients
) throws IOException
, EncapsulatedExceptions
, InvalidNumberException
{
708 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
709 .asEndSessionMessage();
711 final Collection
<SignalServiceAddress
> signalServiceAddresses
= getSignalServiceAddresses(recipients
);
713 sendMessageLegacy(messageBuilder
, signalServiceAddresses
);
714 } catch (Exception e
) {
715 for (SignalServiceAddress address
: signalServiceAddresses
) {
716 handleEndSession(address
);
723 public String
getContactName(String number
) throws InvalidNumberException
{
724 ContactInfo contact
= account
.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number
));
725 if (contact
== null) {
732 public void setContactName(String number
, String name
) throws InvalidNumberException
{
733 final SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(number
);
734 ContactInfo contact
= account
.getContactStore().getContact(address
);
735 if (contact
== null) {
736 contact
= new ContactInfo(address
);
739 account
.getContactStore().updateContact(contact
);
743 public void setContactBlocked(String number
, boolean blocked
) throws InvalidNumberException
{
744 setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number
), blocked
);
747 private void setContactBlocked(SignalServiceAddress address
, boolean blocked
) {
748 ContactInfo contact
= account
.getContactStore().getContact(address
);
749 if (contact
== null) {
750 contact
= new ContactInfo(address
);
752 contact
.blocked
= blocked
;
753 account
.getContactStore().updateContact(contact
);
757 public void setGroupBlocked(final byte[] groupId
, final boolean blocked
) throws GroupNotFoundException
{
758 GroupInfo group
= getGroup(groupId
);
760 throw new GroupNotFoundException(groupId
);
763 group
.blocked
= blocked
;
764 account
.getGroupStore().updateGroup(group
);
768 public byte[] updateGroup(byte[] groupId
, String name
, List
<String
> members
, String avatar
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
, InvalidNumberException
, NotAGroupMemberException
{
769 if (groupId
.length
== 0) {
772 if (name
.isEmpty()) {
775 if (members
.isEmpty()) {
778 if (avatar
.isEmpty()) {
781 return sendUpdateGroupMessage(groupId
, name
, members
== null ?
null : getSignalServiceAddresses(members
), avatar
);
785 * Change the expiration timer for a contact
787 public void setExpirationTimer(SignalServiceAddress address
, int messageExpirationTimer
) throws IOException
{
788 ContactInfo contact
= account
.getContactStore().getContact(address
);
789 contact
.messageExpirationTime
= messageExpirationTimer
;
790 account
.getContactStore().updateContact(contact
);
791 sendExpirationTimerUpdate(address
);
795 private void sendExpirationTimerUpdate(SignalServiceAddress address
) throws IOException
{
796 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
797 .asExpirationUpdate();
798 sendMessage(messageBuilder
, Collections
.singleton(address
));
802 * Change the expiration timer for a contact
804 public void setExpirationTimer(String number
, int messageExpirationTimer
) throws IOException
, InvalidNumberException
{
805 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(number
);
806 setExpirationTimer(address
, messageExpirationTimer
);
810 * Change the expiration timer for a group
812 public void setExpirationTimer(byte[] groupId
, int messageExpirationTimer
) {
813 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
814 g
.messageExpirationTime
= messageExpirationTimer
;
815 account
.getGroupStore().updateGroup(g
);
819 * Upload the sticker pack from path.
821 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
822 * @return if successful, returns the URL to install the sticker pack in the signal app
824 public String
uploadStickerPack(String path
) throws IOException
, StickerPackInvalidException
{
825 SignalServiceStickerManifestUpload manifest
= getSignalServiceStickerManifestUpload(path
);
827 SignalServiceMessageSender messageSender
= getMessageSender();
829 byte[] packKey
= KeyUtils
.createStickerUploadKey();
830 String packId
= messageSender
.uploadStickerManifest(manifest
, packKey
);
833 return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder
.encode(packId
, "utf-8") + "&pack_key=" + URLEncoder
.encode(Hex
.toStringCondensed(packKey
), "utf-8"))
835 } catch (URISyntaxException e
) {
836 throw new AssertionError(e
);
840 private SignalServiceStickerManifestUpload
getSignalServiceStickerManifestUpload(final String path
) throws IOException
, StickerPackInvalidException
{
842 String rootPath
= null;
844 final File file
= new File(path
);
845 if (file
.getName().endsWith(".zip")) {
846 zip
= new ZipFile(file
);
847 } else if (file
.getName().equals("manifest.json")) {
848 rootPath
= file
.getParent();
850 throw new StickerPackInvalidException("Could not find manifest.json");
853 JsonStickerPack pack
= parseStickerPack(rootPath
, zip
);
855 if (pack
.stickers
== null) {
856 throw new StickerPackInvalidException("Must set a 'stickers' field.");
859 if (pack
.stickers
.isEmpty()) {
860 throw new StickerPackInvalidException("Must include stickers.");
863 List
<StickerInfo
> stickers
= new ArrayList
<>(pack
.stickers
.size());
864 for (JsonStickerPack
.JsonSticker sticker
: pack
.stickers
) {
865 if (sticker
.file
== null) {
866 throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
869 Pair
<InputStream
, Long
> data
;
871 data
= getInputStreamAndLength(rootPath
, zip
, sticker
.file
);
872 } catch (IOException ignored
) {
873 throw new StickerPackInvalidException("Could not find find " + sticker
.file
);
876 String contentType
= Utils
.getFileMimeType(new File(sticker
.file
), null);
877 StickerInfo stickerInfo
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(sticker
.emoji
).or(""), contentType
);
878 stickers
.add(stickerInfo
);
881 StickerInfo cover
= null;
882 if (pack
.cover
!= null) {
883 if (pack
.cover
.file
== null) {
884 throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
887 Pair
<InputStream
, Long
> data
;
889 data
= getInputStreamAndLength(rootPath
, zip
, pack
.cover
.file
);
890 } catch (IOException ignored
) {
891 throw new StickerPackInvalidException("Could not find find " + pack
.cover
.file
);
894 String contentType
= Utils
.getFileMimeType(new File(pack
.cover
.file
), null);
895 cover
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(pack
.cover
.emoji
).or(""), contentType
);
898 return new SignalServiceStickerManifestUpload(
905 private static JsonStickerPack
parseStickerPack(String rootPath
, ZipFile zip
) throws IOException
{
906 InputStream inputStream
;
908 inputStream
= zip
.getInputStream(zip
.getEntry("manifest.json"));
910 inputStream
= new FileInputStream((new File(rootPath
, "manifest.json")));
912 return new ObjectMapper().readValue(inputStream
, JsonStickerPack
.class);
915 private static Pair
<InputStream
, Long
> getInputStreamAndLength(final String rootPath
, final ZipFile zip
, final String subfile
) throws IOException
{
917 final ZipEntry entry
= zip
.getEntry(subfile
);
918 return new Pair
<>(zip
.getInputStream(entry
), entry
.getSize());
920 final File file
= new File(rootPath
, subfile
);
921 return new Pair
<>(new FileInputStream(file
), file
.length());
925 void requestSyncGroups() throws IOException
{
926 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
).build();
927 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
929 sendSyncMessage(message
);
930 } catch (UntrustedIdentityException e
) {
935 void requestSyncContacts() throws IOException
{
936 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
).build();
937 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
939 sendSyncMessage(message
);
940 } catch (UntrustedIdentityException e
) {
945 void requestSyncBlocked() throws IOException
{
946 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
).build();
947 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
949 sendSyncMessage(message
);
950 } catch (UntrustedIdentityException e
) {
955 void requestSyncConfiguration() throws IOException
{
956 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
).build();
957 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
959 sendSyncMessage(message
);
960 } catch (UntrustedIdentityException e
) {
965 private byte[] getSenderCertificate() {
966 // TODO support UUID capable sender certificates
967 // byte[] certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy();
970 certificate
= accountManager
.getSenderCertificate();
971 } catch (IOException e
) {
972 System
.err
.println("Failed to get sender certificate: " + e
);
975 // TODO cache for a day
979 private byte[] getSelfUnidentifiedAccessKey() {
980 return UnidentifiedAccess
.deriveAccessKeyFrom(account
.getProfileKey());
983 private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient
) {
984 ContactInfo contact
= account
.getContactStore().getContact(recipient
);
985 if (contact
== null || contact
.profileKey
== null) {
988 ProfileKey theirProfileKey
;
990 theirProfileKey
= new ProfileKey(Base64
.decode(contact
.profileKey
));
991 } catch (InvalidInputException
| IOException e
) {
992 throw new AssertionError(e
);
994 SignalProfile targetProfile
;
996 targetProfile
= getRecipientProfile(recipient
, Optional
.absent(), theirProfileKey
);
997 } catch (IOException e
) {
998 System
.err
.println("Failed to get recipient profile: " + e
);
1002 if (targetProfile
== null || targetProfile
.getUnidentifiedAccess() == null) {
1006 if (targetProfile
.isUnrestrictedUnidentifiedAccess()) {
1007 return KeyUtils
.createUnrestrictedUnidentifiedAccess();
1010 return UnidentifiedAccess
.deriveAccessKeyFrom(theirProfileKey
);
1013 private Optional
<UnidentifiedAccessPair
> getAccessForSync() {
1014 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1015 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1017 if (selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1018 return Optional
.absent();
1022 return Optional
.of(new UnidentifiedAccessPair(
1023 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1024 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1026 } catch (InvalidCertificateException e
) {
1027 return Optional
.absent();
1031 private List
<Optional
<UnidentifiedAccessPair
>> getAccessFor(Collection
<SignalServiceAddress
> recipients
) {
1032 List
<Optional
<UnidentifiedAccessPair
>> result
= new ArrayList
<>(recipients
.size());
1033 for (SignalServiceAddress recipient
: recipients
) {
1034 result
.add(getAccessFor(recipient
));
1039 private Optional
<UnidentifiedAccessPair
> getAccessFor(SignalServiceAddress recipient
) {
1040 byte[] recipientUnidentifiedAccessKey
= getTargetUnidentifiedAccessKey(recipient
);
1041 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1042 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1044 if (recipientUnidentifiedAccessKey
== null || selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1045 return Optional
.absent();
1049 return Optional
.of(new UnidentifiedAccessPair(
1050 new UnidentifiedAccess(recipientUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1051 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1053 } catch (InvalidCertificateException e
) {
1054 return Optional
.absent();
1058 private Optional
<UnidentifiedAccess
> getUnidentifiedAccess(SignalServiceAddress recipient
) {
1059 Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1061 if (unidentifiedAccess
.isPresent()) {
1062 return unidentifiedAccess
.get().getTargetUnidentifiedAccess();
1065 return Optional
.absent();
1068 private void sendSyncMessage(SignalServiceSyncMessage message
)
1069 throws IOException
, UntrustedIdentityException
{
1070 SignalServiceMessageSender messageSender
= getMessageSender();
1072 messageSender
.sendMessage(message
, getAccessForSync());
1073 } catch (UntrustedIdentityException e
) {
1074 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1080 * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult.
1082 private long sendMessageLegacy(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1083 throws EncapsulatedExceptions
, IOException
{
1084 final long timestamp
= System
.currentTimeMillis();
1085 messageBuilder
.withTimestamp(timestamp
);
1086 List
<SendMessageResult
> results
= sendMessage(messageBuilder
, recipients
);
1088 List
<UntrustedIdentityException
> untrustedIdentities
= new LinkedList
<>();
1089 List
<UnregisteredUserException
> unregisteredUsers
= new LinkedList
<>();
1090 List
<NetworkFailureException
> networkExceptions
= new LinkedList
<>();
1092 for (SendMessageResult result
: results
) {
1093 if (result
.isUnregisteredFailure()) {
1094 unregisteredUsers
.add(new UnregisteredUserException(result
.getAddress().getLegacyIdentifier(), null));
1095 } else if (result
.isNetworkFailure()) {
1096 networkExceptions
.add(new NetworkFailureException(result
.getAddress().getLegacyIdentifier(), null));
1097 } else if (result
.getIdentityFailure() != null) {
1098 untrustedIdentities
.add(new UntrustedIdentityException("Untrusted", result
.getAddress().getLegacyIdentifier(), result
.getIdentityFailure().getIdentityKey()));
1101 if (!untrustedIdentities
.isEmpty() || !unregisteredUsers
.isEmpty() || !networkExceptions
.isEmpty()) {
1102 throw new EncapsulatedExceptions(untrustedIdentities
, unregisteredUsers
, networkExceptions
);
1107 private Collection
<SignalServiceAddress
> getSignalServiceAddresses(Collection
<String
> numbers
) throws InvalidNumberException
{
1108 final Set
<SignalServiceAddress
> signalServiceAddresses
= new HashSet
<>(numbers
.size());
1110 for (String number
: numbers
) {
1111 signalServiceAddresses
.add(canonicalizeAndResolveSignalServiceAddress(number
));
1113 return signalServiceAddresses
;
1116 private List
<SendMessageResult
> sendMessage(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1117 throws IOException
{
1118 if (messagePipe
== null) {
1119 messagePipe
= getMessageReceiver().createMessagePipe();
1121 if (unidentifiedMessagePipe
== null) {
1122 unidentifiedMessagePipe
= getMessageReceiver().createUnidentifiedMessagePipe();
1124 SignalServiceDataMessage message
= null;
1126 message
= messageBuilder
.build();
1127 if (message
.getGroupContext().isPresent()) {
1129 SignalServiceMessageSender messageSender
= getMessageSender();
1130 final boolean isRecipientUpdate
= false;
1131 List
<SendMessageResult
> result
= messageSender
.sendMessage(new ArrayList
<>(recipients
), getAccessFor(recipients
), isRecipientUpdate
, message
);
1132 for (SendMessageResult r
: result
) {
1133 if (r
.getIdentityFailure() != null) {
1134 account
.getSignalProtocolStore().saveIdentity(r
.getAddress(), r
.getIdentityFailure().getIdentityKey(), TrustLevel
.UNTRUSTED
);
1138 } catch (UntrustedIdentityException e
) {
1139 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1140 return Collections
.emptyList();
1143 // Send to all individually, so sync messages are sent correctly
1144 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1145 for (SignalServiceAddress address
: recipients
) {
1146 ContactInfo contact
= account
.getContactStore().getContact(address
);
1147 if (contact
!= null) {
1148 messageBuilder
.withExpiration(contact
.messageExpirationTime
);
1149 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
1151 messageBuilder
.withExpiration(0);
1152 messageBuilder
.withProfileKey(null);
1154 message
= messageBuilder
.build();
1155 if (address
.matches(account
.getSelfAddress())) {
1156 results
.add(sendSelfMessage(message
));
1158 results
.add(sendMessage(address
, message
));
1164 if (message
!= null && message
.isEndSession()) {
1165 for (SignalServiceAddress recipient
: recipients
) {
1166 handleEndSession(recipient
);
1173 private SendMessageResult
sendSelfMessage(SignalServiceDataMessage message
) throws IOException
{
1174 SignalServiceMessageSender messageSender
= getMessageSender();
1176 SignalServiceAddress recipient
= account
.getSelfAddress();
1178 final Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1179 SentTranscriptMessage transcript
= new SentTranscriptMessage(Optional
.of(recipient
),
1180 message
.getTimestamp(),
1182 message
.getExpiresInSeconds(),
1183 Collections
.singletonMap(recipient
, unidentifiedAccess
.isPresent()),
1185 SignalServiceSyncMessage syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
1188 messageSender
.sendMessage(syncMessage
, unidentifiedAccess
);
1189 return SendMessageResult
.success(recipient
, unidentifiedAccess
.isPresent(), false);
1190 } catch (UntrustedIdentityException e
) {
1191 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1192 return SendMessageResult
.identityFailure(recipient
, e
.getIdentityKey());
1196 private SendMessageResult
sendMessage(SignalServiceAddress address
, SignalServiceDataMessage message
) throws IOException
{
1197 SignalServiceMessageSender messageSender
= getMessageSender();
1200 return messageSender
.sendMessage(address
, getAccessFor(address
), message
);
1201 } catch (UntrustedIdentityException e
) {
1202 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1203 return SendMessageResult
.identityFailure(address
, e
.getIdentityKey());
1207 private SignalServiceContent
decryptMessage(SignalServiceEnvelope envelope
) throws InvalidMetadataMessageException
, ProtocolInvalidMessageException
, ProtocolDuplicateMessageException
, ProtocolLegacyMessageException
, ProtocolInvalidKeyIdException
, InvalidMetadataVersionException
, ProtocolInvalidVersionException
, ProtocolNoSessionException
, ProtocolInvalidKeyException
, SelfSendException
, UnsupportedDataMessageException
, org
.whispersystems
.libsignal
.UntrustedIdentityException
{
1208 SignalServiceCipher cipher
= new SignalServiceCipher(account
.getSelfAddress(), account
.getSignalProtocolStore(), Utils
.getCertificateValidator());
1210 return cipher
.decrypt(envelope
);
1211 } catch (ProtocolUntrustedIdentityException e
) {
1212 if (e
.getCause() instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
) {
1213 org
.whispersystems
.libsignal
.UntrustedIdentityException identityException
= (org
.whispersystems
.libsignal
.UntrustedIdentityException
) e
.getCause();
1214 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(identityException
.getName()), identityException
.getUntrustedIdentity(), TrustLevel
.UNTRUSTED
);
1215 throw identityException
;
1217 throw new AssertionError(e
);
1221 private void handleEndSession(SignalServiceAddress source
) {
1222 account
.getSignalProtocolStore().deleteAllSessions(source
);
1225 private List
<HandleAction
> handleSignalServiceDataMessage(SignalServiceDataMessage message
, boolean isSync
, SignalServiceAddress source
, SignalServiceAddress destination
, boolean ignoreAttachments
) {
1226 List
<HandleAction
> actions
= new ArrayList
<>();
1227 if (message
.getGroupContext().isPresent() && message
.getGroupContext().get().getGroupV1().isPresent()) {
1228 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1229 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1230 switch (groupInfo
.getType()) {
1232 if (group
== null) {
1233 group
= new GroupInfo(groupInfo
.getGroupId());
1236 if (groupInfo
.getAvatar().isPresent()) {
1237 SignalServiceAttachment avatar
= groupInfo
.getAvatar().get();
1238 if (avatar
.isPointer()) {
1240 retrieveGroupAvatarAttachment(avatar
.asPointer(), group
.groupId
);
1241 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1242 System
.err
.println("Failed to retrieve group avatar (" + avatar
.asPointer().getRemoteId() + "): " + e
.getMessage());
1247 if (groupInfo
.getName().isPresent()) {
1248 group
.name
= groupInfo
.getName().get();
1251 if (groupInfo
.getMembers().isPresent()) {
1252 group
.addMembers(groupInfo
.getMembers().get()
1254 .map(this::resolveSignalServiceAddress
)
1255 .collect(Collectors
.toSet()));
1258 account
.getGroupStore().updateGroup(group
);
1261 if (group
== null && !isSync
) {
1262 actions
.add(new SendGroupInfoRequestAction(source
, groupInfo
.getGroupId()));
1266 if (group
!= null) {
1267 group
.removeMember(source
);
1268 account
.getGroupStore().updateGroup(group
);
1272 if (group
!= null && !isSync
) {
1273 actions
.add(new SendGroupUpdateAction(source
, group
.groupId
));
1278 final SignalServiceAddress conversationPartnerAddress
= isSync ? destination
: source
;
1279 if (message
.isEndSession()) {
1280 handleEndSession(conversationPartnerAddress
);
1282 if (message
.isExpirationUpdate() || message
.getBody().isPresent()) {
1283 if (message
.getGroupContext().isPresent() && message
.getGroupContext().get().getGroupV1().isPresent()) {
1284 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1285 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1286 if (group
== null) {
1287 group
= new GroupInfo(groupInfo
.getGroupId());
1289 if (group
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1290 group
.messageExpirationTime
= message
.getExpiresInSeconds();
1291 account
.getGroupStore().updateGroup(group
);
1294 ContactInfo contact
= account
.getContactStore().getContact(conversationPartnerAddress
);
1295 if (contact
== null) {
1296 contact
= new ContactInfo(conversationPartnerAddress
);
1298 if (contact
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1299 contact
.messageExpirationTime
= message
.getExpiresInSeconds();
1300 account
.getContactStore().updateContact(contact
);
1304 if (message
.getAttachments().isPresent() && !ignoreAttachments
) {
1305 for (SignalServiceAttachment attachment
: message
.getAttachments().get()) {
1306 if (attachment
.isPointer()) {
1308 retrieveAttachment(attachment
.asPointer());
1309 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1310 System
.err
.println("Failed to retrieve attachment (" + attachment
.asPointer().getRemoteId() + "): " + e
.getMessage());
1315 if (message
.getProfileKey().isPresent() && message
.getProfileKey().get().length
== 32) {
1316 if (source
.matches(account
.getSelfAddress())) {
1318 this.account
.setProfileKey(new ProfileKey(message
.getProfileKey().get()));
1319 } catch (InvalidInputException ignored
) {
1321 ContactInfo contact
= account
.getContactStore().getContact(source
);
1322 if (contact
!= null) {
1323 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1324 account
.getContactStore().updateContact(contact
);
1327 ContactInfo contact
= account
.getContactStore().getContact(source
);
1328 if (contact
== null) {
1329 contact
= new ContactInfo(source
);
1331 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1332 account
.getContactStore().updateContact(contact
);
1335 if (message
.getPreviews().isPresent()) {
1336 final List
<SignalServiceDataMessage
.Preview
> previews
= message
.getPreviews().get();
1337 for (SignalServiceDataMessage
.Preview preview
: previews
) {
1338 if (preview
.getImage().isPresent() && preview
.getImage().get().isPointer()) {
1339 SignalServiceAttachmentPointer attachment
= preview
.getImage().get().asPointer();
1341 retrieveAttachment(attachment
);
1342 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1343 System
.err
.println("Failed to retrieve attachment (" + attachment
.getRemoteId() + "): " + e
.getMessage());
1351 private void retryFailedReceivedMessages(ReceiveMessageHandler handler
, boolean ignoreAttachments
) {
1352 final File cachePath
= new File(getMessageCachePath());
1353 if (!cachePath
.exists()) {
1356 for (final File dir
: Objects
.requireNonNull(cachePath
.listFiles())) {
1357 if (!dir
.isDirectory()) {
1358 retryFailedReceivedMessage(handler
, ignoreAttachments
, dir
);
1362 for (final File fileEntry
: Objects
.requireNonNull(dir
.listFiles())) {
1363 if (!fileEntry
.isFile()) {
1366 retryFailedReceivedMessage(handler
, ignoreAttachments
, fileEntry
);
1368 // Try to delete directory if empty
1373 private void retryFailedReceivedMessage(final ReceiveMessageHandler handler
, final boolean ignoreAttachments
, final File fileEntry
) {
1374 SignalServiceEnvelope envelope
;
1376 envelope
= Utils
.loadEnvelope(fileEntry
);
1377 if (envelope
== null) {
1380 } catch (IOException e
) {
1381 e
.printStackTrace();
1384 SignalServiceContent content
= null;
1385 if (!envelope
.isReceipt()) {
1387 content
= decryptMessage(envelope
);
1388 } catch (Exception e
) {
1391 List
<HandleAction
> actions
= handleMessage(envelope
, content
, ignoreAttachments
);
1392 for (HandleAction action
: actions
) {
1394 action
.execute(this);
1395 } catch (Throwable e
) {
1396 e
.printStackTrace();
1401 handler
.handleMessage(envelope
, content
, null);
1403 Files
.delete(fileEntry
.toPath());
1404 } catch (IOException e
) {
1405 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1409 public void receiveMessages(long timeout
, TimeUnit unit
, boolean returnOnTimeout
, boolean ignoreAttachments
, ReceiveMessageHandler handler
) throws IOException
{
1410 retryFailedReceivedMessages(handler
, ignoreAttachments
);
1411 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1413 Set
<HandleAction
> queuedActions
= null;
1415 if (messagePipe
== null) {
1416 messagePipe
= messageReceiver
.createMessagePipe();
1419 boolean hasCaughtUpWithOldMessages
= false;
1422 SignalServiceEnvelope envelope
;
1423 SignalServiceContent content
= null;
1424 Exception exception
= null;
1425 final long now
= new Date().getTime();
1427 Optional
<SignalServiceEnvelope
> result
= messagePipe
.readOrEmpty(timeout
, unit
, envelope1
-> {
1428 // store message on disk, before acknowledging receipt to the server
1430 String source
= envelope1
.getSourceE164().isPresent() ? envelope1
.getSourceE164().get() : "";
1431 File cacheFile
= getMessageCacheFile(source
, now
, envelope1
.getTimestamp());
1432 Utils
.storeEnvelope(envelope1
, cacheFile
);
1433 } catch (IOException e
) {
1434 System
.err
.println("Failed to store encrypted message in disk cache, ignoring: " + e
.getMessage());
1437 if (result
.isPresent()) {
1438 envelope
= result
.get();
1440 // Received indicator that server queue is empty
1441 hasCaughtUpWithOldMessages
= true;
1443 if (queuedActions
!= null) {
1444 for (HandleAction action
: queuedActions
) {
1446 action
.execute(this);
1447 } catch (Throwable e
) {
1448 e
.printStackTrace();
1451 queuedActions
.clear();
1452 queuedActions
= null;
1455 // Continue to wait another timeout for new messages
1458 } catch (TimeoutException e
) {
1459 if (returnOnTimeout
)
1462 } catch (InvalidVersionException e
) {
1463 System
.err
.println("Ignoring error: " + e
.getMessage());
1466 if (envelope
.hasSource()) {
1467 // Store uuid if we don't have it already
1468 SignalServiceAddress source
= envelope
.getSourceAddress();
1469 resolveSignalServiceAddress(source
);
1471 if (!envelope
.isReceipt()) {
1473 content
= decryptMessage(envelope
);
1474 } catch (Exception e
) {
1477 List
<HandleAction
> actions
= handleMessage(envelope
, content
, ignoreAttachments
);
1478 if (hasCaughtUpWithOldMessages
) {
1479 for (HandleAction action
: actions
) {
1481 action
.execute(this);
1482 } catch (Throwable e
) {
1483 e
.printStackTrace();
1487 if (queuedActions
== null) {
1488 queuedActions
= new HashSet
<>();
1490 queuedActions
.addAll(actions
);
1494 if (!isMessageBlocked(envelope
, content
)) {
1495 handler
.handleMessage(envelope
, content
, exception
);
1497 if (!(exception
instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
)) {
1498 File cacheFile
= null;
1500 cacheFile
= getMessageCacheFile(envelope
.getSourceE164().get(), now
, envelope
.getTimestamp());
1501 Files
.delete(cacheFile
.toPath());
1502 // Try to delete directory if empty
1503 new File(getMessageCachePath()).delete();
1504 } catch (IOException e
) {
1505 System
.err
.println("Failed to delete cached message file “" + cacheFile
+ "”: " + e
.getMessage());
1511 private boolean isMessageBlocked(SignalServiceEnvelope envelope
, SignalServiceContent content
) {
1512 SignalServiceAddress source
;
1513 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1514 source
= envelope
.getSourceAddress();
1515 } else if (content
!= null) {
1516 source
= content
.getSender();
1520 ContactInfo sourceContact
= account
.getContactStore().getContact(source
);
1521 if (sourceContact
!= null && sourceContact
.blocked
) {
1525 if (content
!= null && content
.getDataMessage().isPresent()) {
1526 SignalServiceDataMessage message
= content
.getDataMessage().get();
1527 if (message
.getGroupContext().isPresent() && message
.getGroupContext().get().getGroupV1().isPresent()) {
1528 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1529 GroupInfo group
= getGroup(groupInfo
.getGroupId());
1530 if (groupInfo
.getType() == SignalServiceGroup
.Type
.DELIVER
&& group
!= null && group
.blocked
) {
1538 private List
<HandleAction
> handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent content
, boolean ignoreAttachments
) {
1539 List
<HandleAction
> actions
= new ArrayList
<>();
1540 if (content
!= null) {
1541 SignalServiceAddress sender
;
1542 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1543 sender
= envelope
.getSourceAddress();
1545 sender
= content
.getSender();
1547 // Store uuid if we don't have it already
1548 resolveSignalServiceAddress(sender
);
1550 if (content
.getDataMessage().isPresent()) {
1551 SignalServiceDataMessage message
= content
.getDataMessage().get();
1553 if (content
.isNeedsReceipt()) {
1554 actions
.add(new SendReceiptAction(sender
, message
.getTimestamp()));
1557 actions
.addAll(handleSignalServiceDataMessage(message
, false, sender
, account
.getSelfAddress(), ignoreAttachments
));
1559 if (content
.getSyncMessage().isPresent()) {
1560 account
.setMultiDevice(true);
1561 SignalServiceSyncMessage syncMessage
= content
.getSyncMessage().get();
1562 if (syncMessage
.getSent().isPresent()) {
1563 SentTranscriptMessage message
= syncMessage
.getSent().get();
1564 actions
.addAll(handleSignalServiceDataMessage(message
.getMessage(), true, sender
, message
.getDestination().orNull(), ignoreAttachments
));
1566 if (syncMessage
.getRequest().isPresent()) {
1567 RequestMessage rm
= syncMessage
.getRequest().get();
1568 if (rm
.isContactsRequest()) {
1569 actions
.add(SendSyncContactsAction
.create());
1571 if (rm
.isGroupsRequest()) {
1572 actions
.add(SendSyncGroupsAction
.create());
1574 if (rm
.isBlockedListRequest()) {
1575 actions
.add(SendSyncBlockedListAction
.create());
1577 // TODO Handle rm.isConfigurationRequest();
1579 if (syncMessage
.getGroups().isPresent()) {
1580 File tmpFile
= null;
1582 tmpFile
= IOUtils
.createTempFile();
1583 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(syncMessage
.getGroups().get().asPointer(), tmpFile
)) {
1584 DeviceGroupsInputStream s
= new DeviceGroupsInputStream(attachmentAsStream
);
1586 while ((g
= s
.read()) != null) {
1587 GroupInfo syncGroup
= account
.getGroupStore().getGroup(g
.getId());
1588 if (syncGroup
== null) {
1589 syncGroup
= new GroupInfo(g
.getId());
1591 if (g
.getName().isPresent()) {
1592 syncGroup
.name
= g
.getName().get();
1594 syncGroup
.addMembers(g
.getMembers()
1596 .map(this::resolveSignalServiceAddress
)
1597 .collect(Collectors
.toSet()));
1598 if (!g
.isActive()) {
1599 syncGroup
.removeMember(account
.getSelfAddress());
1601 // Add ourself to the member set as it's marked as active
1602 syncGroup
.addMembers(Collections
.singleton(account
.getSelfAddress()));
1604 syncGroup
.blocked
= g
.isBlocked();
1605 if (g
.getColor().isPresent()) {
1606 syncGroup
.color
= g
.getColor().get();
1609 if (g
.getAvatar().isPresent()) {
1610 retrieveGroupAvatarAttachment(g
.getAvatar().get(), syncGroup
.groupId
);
1612 syncGroup
.inboxPosition
= g
.getInboxPosition().orNull();
1613 syncGroup
.archived
= g
.isArchived();
1614 account
.getGroupStore().updateGroup(syncGroup
);
1617 } catch (Exception e
) {
1618 e
.printStackTrace();
1620 if (tmpFile
!= null) {
1622 Files
.delete(tmpFile
.toPath());
1623 } catch (IOException e
) {
1624 System
.err
.println("Failed to delete received groups temp file “" + tmpFile
+ "”: " + e
.getMessage());
1629 if (syncMessage
.getBlockedList().isPresent()) {
1630 final BlockedListMessage blockedListMessage
= syncMessage
.getBlockedList().get();
1631 for (SignalServiceAddress address
: blockedListMessage
.getAddresses()) {
1632 setContactBlocked(resolveSignalServiceAddress(address
), true);
1634 for (byte[] groupId
: blockedListMessage
.getGroupIds()) {
1636 setGroupBlocked(groupId
, true);
1637 } catch (GroupNotFoundException e
) {
1638 System
.err
.println("BlockedListMessage contained groupID that was not found in GroupStore: " + Base64
.encodeBytes(groupId
));
1642 if (syncMessage
.getContacts().isPresent()) {
1643 File tmpFile
= null;
1645 tmpFile
= IOUtils
.createTempFile();
1646 final ContactsMessage contactsMessage
= syncMessage
.getContacts().get();
1647 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(contactsMessage
.getContactsStream().asPointer(), tmpFile
)) {
1648 DeviceContactsInputStream s
= new DeviceContactsInputStream(attachmentAsStream
);
1649 if (contactsMessage
.isComplete()) {
1650 account
.getContactStore().clear();
1653 while ((c
= s
.read()) != null) {
1654 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
1655 account
.setProfileKey(c
.getProfileKey().get());
1657 final SignalServiceAddress address
= resolveSignalServiceAddress(c
.getAddress());
1658 ContactInfo contact
= account
.getContactStore().getContact(address
);
1659 if (contact
== null) {
1660 contact
= new ContactInfo(address
);
1662 if (c
.getName().isPresent()) {
1663 contact
.name
= c
.getName().get();
1665 if (c
.getColor().isPresent()) {
1666 contact
.color
= c
.getColor().get();
1668 if (c
.getProfileKey().isPresent()) {
1669 contact
.profileKey
= Base64
.encodeBytes(c
.getProfileKey().get().serialize());
1671 if (c
.getVerified().isPresent()) {
1672 final VerifiedMessage verifiedMessage
= c
.getVerified().get();
1673 account
.getSignalProtocolStore().setIdentityTrustLevel(verifiedMessage
.getDestination(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1675 if (c
.getExpirationTimer().isPresent()) {
1676 contact
.messageExpirationTime
= c
.getExpirationTimer().get();
1678 contact
.blocked
= c
.isBlocked();
1679 contact
.inboxPosition
= c
.getInboxPosition().orNull();
1680 contact
.archived
= c
.isArchived();
1681 account
.getContactStore().updateContact(contact
);
1683 if (c
.getAvatar().isPresent()) {
1684 retrieveContactAvatarAttachment(c
.getAvatar().get(), contact
.number
);
1688 } catch (Exception e
) {
1689 e
.printStackTrace();
1691 if (tmpFile
!= null) {
1693 Files
.delete(tmpFile
.toPath());
1694 } catch (IOException e
) {
1695 System
.err
.println("Failed to delete received contacts temp file “" + tmpFile
+ "”: " + e
.getMessage());
1700 if (syncMessage
.getVerified().isPresent()) {
1701 final VerifiedMessage verifiedMessage
= syncMessage
.getVerified().get();
1702 account
.getSignalProtocolStore().setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage
.getDestination()), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1704 if (syncMessage
.getConfiguration().isPresent()) {
1712 private File
getContactAvatarFile(String number
) {
1713 return new File(pathConfig
.getAvatarsPath(), "contact-" + number
);
1716 private File
retrieveContactAvatarAttachment(SignalServiceAttachment attachment
, String number
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1717 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
1718 if (attachment
.isPointer()) {
1719 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1720 return retrieveAttachment(pointer
, getContactAvatarFile(number
), false);
1722 SignalServiceAttachmentStream stream
= attachment
.asStream();
1723 return Utils
.retrieveAttachment(stream
, getContactAvatarFile(number
));
1727 private File
getGroupAvatarFile(byte[] groupId
) {
1728 return new File(pathConfig
.getAvatarsPath(), "group-" + Base64
.encodeBytes(groupId
).replace("/", "_"));
1731 private File
retrieveGroupAvatarAttachment(SignalServiceAttachment attachment
, byte[] groupId
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1732 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
1733 if (attachment
.isPointer()) {
1734 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1735 return retrieveAttachment(pointer
, getGroupAvatarFile(groupId
), false);
1737 SignalServiceAttachmentStream stream
= attachment
.asStream();
1738 return Utils
.retrieveAttachment(stream
, getGroupAvatarFile(groupId
));
1742 private File
getProfileAvatarFile(SignalServiceAddress address
) {
1743 return new File(pathConfig
.getAvatarsPath(), "profile-" + address
.getLegacyIdentifier());
1746 private File
retrieveProfileAvatar(SignalServiceAddress address
, String avatarPath
, ProfileKey profileKey
) throws IOException
{
1747 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
1748 SignalServiceMessageReceiver receiver
= getMessageReceiver();
1749 File outputFile
= getProfileAvatarFile(address
);
1751 File tmpFile
= IOUtils
.createTempFile();
1752 try (InputStream input
= receiver
.retrieveProfileAvatar(avatarPath
, tmpFile
, profileKey
, ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
)) {
1753 // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
1754 IOUtils
.copyStreamToFile(input
, outputFile
, (int) ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
);
1757 Files
.delete(tmpFile
.toPath());
1758 } catch (IOException e
) {
1759 System
.err
.println("Failed to delete received avatar temp file “" + tmpFile
+ "”: " + e
.getMessage());
1765 public File
getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId
) {
1766 return new File(pathConfig
.getAttachmentsPath(), attachmentId
.toString());
1769 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1770 IOUtils
.createPrivateDirectories(pathConfig
.getAttachmentsPath());
1771 return retrieveAttachment(pointer
, getAttachmentFile(pointer
.getRemoteId()), true);
1774 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
, File outputFile
, boolean storePreview
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1775 if (storePreview
&& pointer
.getPreview().isPresent()) {
1776 File previewFile
= new File(outputFile
+ ".preview");
1777 try (OutputStream output
= new FileOutputStream(previewFile
)) {
1778 byte[] preview
= pointer
.getPreview().get();
1779 output
.write(preview
, 0, preview
.length
);
1780 } catch (FileNotFoundException e
) {
1781 e
.printStackTrace();
1786 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1788 File tmpFile
= IOUtils
.createTempFile();
1789 try (InputStream input
= messageReceiver
.retrieveAttachment(pointer
, tmpFile
, ServiceConfig
.MAX_ATTACHMENT_SIZE
)) {
1790 IOUtils
.copyStreamToFile(input
, outputFile
);
1793 Files
.delete(tmpFile
.toPath());
1794 } catch (IOException e
) {
1795 System
.err
.println("Failed to delete received attachment temp file “" + tmpFile
+ "”: " + e
.getMessage());
1801 private InputStream
retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer
, File tmpFile
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1802 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1803 return messageReceiver
.retrieveAttachment(pointer
, tmpFile
, ServiceConfig
.MAX_ATTACHMENT_SIZE
);
1806 void sendGroups() throws IOException
, UntrustedIdentityException
{
1807 File groupsFile
= IOUtils
.createTempFile();
1810 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
1811 DeviceGroupsOutputStream out
= new DeviceGroupsOutputStream(fos
);
1812 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1813 out
.write(new DeviceGroup(record.groupId
, Optional
.fromNullable(record.name
),
1814 new ArrayList
<>(record.getMembers()), createGroupAvatarAttachment(record.groupId
),
1815 record.isMember(account
.getSelfAddress()), Optional
.of(record.messageExpirationTime
),
1816 Optional
.fromNullable(record.color
), record.blocked
, Optional
.fromNullable(record.inboxPosition
), record.archived
));
1820 if (groupsFile
.exists() && groupsFile
.length() > 0) {
1821 try (FileInputStream groupsFileStream
= new FileInputStream(groupsFile
)) {
1822 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1823 .withStream(groupsFileStream
)
1824 .withContentType("application/octet-stream")
1825 .withLength(groupsFile
.length())
1828 sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
1833 Files
.delete(groupsFile
.toPath());
1834 } catch (IOException e
) {
1835 System
.err
.println("Failed to delete groups temp file “" + groupsFile
+ "”: " + e
.getMessage());
1840 public void sendContacts() throws IOException
, UntrustedIdentityException
{
1841 File contactsFile
= IOUtils
.createTempFile();
1844 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
1845 DeviceContactsOutputStream out
= new DeviceContactsOutputStream(fos
);
1846 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1847 VerifiedMessage verifiedMessage
= null;
1848 JsonIdentityKeyStore
.Identity currentIdentity
= account
.getSignalProtocolStore().getIdentity(record.getAddress());
1849 if (currentIdentity
!= null) {
1850 verifiedMessage
= new VerifiedMessage(record.getAddress(), currentIdentity
.getIdentityKey(), currentIdentity
.getTrustLevel().toVerifiedState(), currentIdentity
.getDateAdded().getTime());
1853 ProfileKey profileKey
= null;
1855 profileKey
= record.profileKey
== null ?
null : new ProfileKey(Base64
.decode(record.profileKey
));
1856 } catch (InvalidInputException ignored
) {
1858 out
.write(new DeviceContact(record.getAddress(), Optional
.fromNullable(record.name
),
1859 createContactAvatarAttachment(record.number
), Optional
.fromNullable(record.color
),
1860 Optional
.fromNullable(verifiedMessage
), Optional
.fromNullable(profileKey
), record.blocked
,
1861 Optional
.of(record.messageExpirationTime
),
1862 Optional
.fromNullable(record.inboxPosition
), record.archived
));
1865 if (account
.getProfileKey() != null) {
1866 // Send our own profile key as well
1867 out
.write(new DeviceContact(account
.getSelfAddress(),
1868 Optional
.absent(), Optional
.absent(),
1869 Optional
.absent(), Optional
.absent(),
1870 Optional
.of(account
.getProfileKey()),
1871 false, Optional
.absent(), Optional
.absent(), false));
1875 if (contactsFile
.exists() && contactsFile
.length() > 0) {
1876 try (FileInputStream contactsFileStream
= new FileInputStream(contactsFile
)) {
1877 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1878 .withStream(contactsFileStream
)
1879 .withContentType("application/octet-stream")
1880 .withLength(contactsFile
.length())
1883 sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
, true)));
1888 Files
.delete(contactsFile
.toPath());
1889 } catch (IOException e
) {
1890 System
.err
.println("Failed to delete contacts temp file “" + contactsFile
+ "”: " + e
.getMessage());
1895 void sendBlockedList() throws IOException
, UntrustedIdentityException
{
1896 List
<SignalServiceAddress
> addresses
= new ArrayList
<>();
1897 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1898 if (record.blocked
) {
1899 addresses
.add(record.getAddress());
1902 List
<byte[]> groupIds
= new ArrayList
<>();
1903 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1904 if (record.blocked
) {
1905 groupIds
.add(record.groupId
);
1908 sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
1911 private void sendVerifiedMessage(SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
) throws IOException
, UntrustedIdentityException
{
1912 VerifiedMessage verifiedMessage
= new VerifiedMessage(destination
, identityKey
, trustLevel
.toVerifiedState(), System
.currentTimeMillis());
1913 sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
1916 public List
<ContactInfo
> getContacts() {
1917 return account
.getContactStore().getContacts();
1920 public ContactInfo
getContact(String number
) {
1921 return account
.getContactStore().getContact(Util
.getSignalServiceAddressFromIdentifier(number
));
1924 public GroupInfo
getGroup(byte[] groupId
) {
1925 return account
.getGroupStore().getGroup(groupId
);
1928 public List
<JsonIdentityKeyStore
.Identity
> getIdentities() {
1929 return account
.getSignalProtocolStore().getIdentities();
1932 public List
<JsonIdentityKeyStore
.Identity
> getIdentities(String number
) throws InvalidNumberException
{
1933 return account
.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number
));
1937 * Trust this the identity with this fingerprint
1939 * @param name username of the identity
1940 * @param fingerprint Fingerprint
1942 public boolean trustIdentityVerified(String name
, byte[] fingerprint
) throws InvalidNumberException
{
1943 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
1944 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
1948 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1949 if (!Arrays
.equals(id
.getIdentityKey().serialize(), fingerprint
)) {
1953 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1955 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1956 } catch (IOException
| UntrustedIdentityException e
) {
1957 e
.printStackTrace();
1966 * Trust this the identity with this safety number
1968 * @param name username of the identity
1969 * @param safetyNumber Safety number
1971 public boolean trustIdentityVerifiedSafetyNumber(String name
, String safetyNumber
) throws InvalidNumberException
{
1972 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
1973 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
1977 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1978 if (!safetyNumber
.equals(computeSafetyNumber(address
, id
.getIdentityKey()))) {
1982 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1984 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1985 } catch (IOException
| UntrustedIdentityException e
) {
1986 e
.printStackTrace();
1995 * Trust all keys of this identity without verification
1997 * @param name username of the identity
1999 public boolean trustIdentityAllKeys(String name
) {
2000 SignalServiceAddress address
= resolveSignalServiceAddress(name
);
2001 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2005 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2006 if (id
.getTrustLevel() == TrustLevel
.UNTRUSTED
) {
2007 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2009 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2010 } catch (IOException
| UntrustedIdentityException e
) {
2011 e
.printStackTrace();
2019 public String
computeSafetyNumber(SignalServiceAddress theirAddress
, IdentityKey theirIdentityKey
) {
2020 return Utils
.computeSafetyNumber(account
.getSelfAddress(), getIdentityKeyPair().getPublicKey(), theirAddress
, theirIdentityKey
);
2023 void saveAccount() {
2027 public SignalServiceAddress
canonicalizeAndResolveSignalServiceAddress(String identifier
) throws InvalidNumberException
{
2028 String canonicalizedNumber
= UuidUtil
.isUuid(identifier
) ? identifier
: Util
.canonicalizeNumber(identifier
, account
.getUsername());
2029 return resolveSignalServiceAddress(canonicalizedNumber
);
2032 public SignalServiceAddress
resolveSignalServiceAddress(String identifier
) {
2033 SignalServiceAddress address
= Util
.getSignalServiceAddressFromIdentifier(identifier
);
2035 return resolveSignalServiceAddress(address
);
2038 public SignalServiceAddress
resolveSignalServiceAddress(SignalServiceAddress address
) {
2039 if (address
.matches(account
.getSelfAddress())) {
2040 return account
.getSelfAddress();
2043 return account
.getRecipientStore().resolveServiceAddress(address
);
2047 public void close() throws IOException
{
2048 if (messagePipe
!= null) {
2049 messagePipe
.shutdown();
2053 if (unidentifiedMessagePipe
!= null) {
2054 unidentifiedMessagePipe
.shutdown();
2055 unidentifiedMessagePipe
= null;
2061 public interface ReceiveMessageHandler
{
2063 void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent decryptedContent
, Throwable e
);