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
;
22 import org
.asamk
.signal
.AttachmentInvalidException
;
23 import org
.asamk
.signal
.GroupNotFoundException
;
24 import org
.asamk
.signal
.NotAGroupMemberException
;
25 import org
.asamk
.signal
.StickerPackInvalidException
;
26 import org
.asamk
.signal
.TrustLevel
;
27 import org
.asamk
.signal
.UserAlreadyExists
;
28 import org
.asamk
.signal
.storage
.SignalAccount
;
29 import org
.asamk
.signal
.storage
.contacts
.ContactInfo
;
30 import org
.asamk
.signal
.storage
.groups
.GroupInfo
;
31 import org
.asamk
.signal
.storage
.groups
.JsonGroupStore
;
32 import org
.asamk
.signal
.storage
.protocol
.JsonIdentityKeyStore
;
33 import org
.asamk
.signal
.util
.IOUtils
;
34 import org
.asamk
.signal
.util
.Util
;
35 import org
.signal
.libsignal
.metadata
.InvalidMetadataMessageException
;
36 import org
.signal
.libsignal
.metadata
.InvalidMetadataVersionException
;
37 import org
.signal
.libsignal
.metadata
.ProtocolDuplicateMessageException
;
38 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyException
;
39 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyIdException
;
40 import org
.signal
.libsignal
.metadata
.ProtocolInvalidMessageException
;
41 import org
.signal
.libsignal
.metadata
.ProtocolInvalidVersionException
;
42 import org
.signal
.libsignal
.metadata
.ProtocolLegacyMessageException
;
43 import org
.signal
.libsignal
.metadata
.ProtocolNoSessionException
;
44 import org
.signal
.libsignal
.metadata
.ProtocolUntrustedIdentityException
;
45 import org
.signal
.libsignal
.metadata
.SelfSendException
;
46 import org
.signal
.libsignal
.metadata
.certificate
.InvalidCertificateException
;
47 import org
.signal
.zkgroup
.InvalidInputException
;
48 import org
.signal
.zkgroup
.VerificationFailedException
;
49 import org
.signal
.zkgroup
.profiles
.ClientZkProfileOperations
;
50 import org
.signal
.zkgroup
.profiles
.ProfileKey
;
51 import org
.whispersystems
.libsignal
.IdentityKey
;
52 import org
.whispersystems
.libsignal
.IdentityKeyPair
;
53 import org
.whispersystems
.libsignal
.InvalidKeyException
;
54 import org
.whispersystems
.libsignal
.InvalidMessageException
;
55 import org
.whispersystems
.libsignal
.InvalidVersionException
;
56 import org
.whispersystems
.libsignal
.ecc
.Curve
;
57 import org
.whispersystems
.libsignal
.ecc
.ECKeyPair
;
58 import org
.whispersystems
.libsignal
.ecc
.ECPublicKey
;
59 import org
.whispersystems
.libsignal
.state
.PreKeyRecord
;
60 import org
.whispersystems
.libsignal
.state
.SignedPreKeyRecord
;
61 import org
.whispersystems
.libsignal
.util
.KeyHelper
;
62 import org
.whispersystems
.libsignal
.util
.Medium
;
63 import org
.whispersystems
.libsignal
.util
.Pair
;
64 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
65 import org
.whispersystems
.signalservice
.api
.SignalServiceAccountManager
;
66 import org
.whispersystems
.signalservice
.api
.SignalServiceMessagePipe
;
67 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageReceiver
;
68 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageSender
;
69 import org
.whispersystems
.signalservice
.api
.crypto
.InvalidCiphertextException
;
70 import org
.whispersystems
.signalservice
.api
.crypto
.ProfileCipher
;
71 import org
.whispersystems
.signalservice
.api
.crypto
.SignalServiceCipher
;
72 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccess
;
73 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccessPair
;
74 import org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException
;
75 import org
.whispersystems
.signalservice
.api
.messages
.SendMessageResult
;
76 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachment
;
77 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentPointer
;
78 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentRemoteId
;
79 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentStream
;
80 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceContent
;
81 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceDataMessage
;
82 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceEnvelope
;
83 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroup
;
84 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceReceiptMessage
;
85 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceStickerManifestUpload
;
86 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceStickerManifestUpload
.StickerInfo
;
87 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.BlockedListMessage
;
88 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ContactsMessage
;
89 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContact
;
90 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsInputStream
;
91 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsOutputStream
;
92 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroup
;
93 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsInputStream
;
94 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsOutputStream
;
95 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceInfo
;
96 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.RequestMessage
;
97 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SentTranscriptMessage
;
98 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SignalServiceSyncMessage
;
99 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.VerifiedMessage
;
100 import org
.whispersystems
.signalservice
.api
.profiles
.SignalServiceProfile
;
101 import org
.whispersystems
.signalservice
.api
.push
.ContactTokenDetails
;
102 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
103 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.EncapsulatedExceptions
;
104 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.MissingConfigurationException
;
105 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NetworkFailureException
;
106 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.UnregisteredUserException
;
107 import org
.whispersystems
.signalservice
.api
.util
.InvalidNumberException
;
108 import org
.whispersystems
.signalservice
.api
.util
.SleepTimer
;
109 import org
.whispersystems
.signalservice
.api
.util
.StreamDetails
;
110 import org
.whispersystems
.signalservice
.api
.util
.UptimeSleepTimer
;
111 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
112 import org
.whispersystems
.signalservice
.internal
.configuration
.SignalServiceConfiguration
;
113 import org
.whispersystems
.signalservice
.internal
.push
.SignalServiceProtos
;
114 import org
.whispersystems
.signalservice
.internal
.push
.UnsupportedDataMessageException
;
115 import org
.whispersystems
.signalservice
.internal
.push
.VerifyAccountResponse
;
116 import org
.whispersystems
.signalservice
.internal
.util
.Hex
;
117 import org
.whispersystems
.util
.Base64
;
120 import java
.io
.FileInputStream
;
121 import java
.io
.FileNotFoundException
;
122 import java
.io
.FileOutputStream
;
123 import java
.io
.IOException
;
124 import java
.io
.InputStream
;
125 import java
.io
.OutputStream
;
127 import java
.net
.URISyntaxException
;
128 import java
.net
.URLEncoder
;
129 import java
.nio
.file
.Files
;
130 import java
.nio
.file
.Paths
;
131 import java
.nio
.file
.StandardCopyOption
;
132 import java
.util
.ArrayList
;
133 import java
.util
.Arrays
;
134 import java
.util
.Collection
;
135 import java
.util
.Collections
;
136 import java
.util
.Date
;
137 import java
.util
.HashSet
;
138 import java
.util
.LinkedList
;
139 import java
.util
.List
;
140 import java
.util
.Locale
;
141 import java
.util
.Objects
;
142 import java
.util
.Set
;
143 import java
.util
.UUID
;
144 import java
.util
.concurrent
.TimeUnit
;
145 import java
.util
.concurrent
.TimeoutException
;
146 import java
.util
.stream
.Collectors
;
147 import java
.util
.zip
.ZipEntry
;
148 import java
.util
.zip
.ZipFile
;
150 public class Manager
implements Signal
{
152 private final String settingsPath
;
153 private final String dataPath
;
154 private final String attachmentsPath
;
155 private final String avatarsPath
;
156 private final SleepTimer timer
= new UptimeSleepTimer();
157 private final SignalServiceConfiguration serviceConfiguration
;
158 private final String userAgent
;
160 private SignalAccount account
;
161 private String username
;
162 private SignalServiceAccountManager accountManager
;
163 private SignalServiceMessagePipe messagePipe
= null;
164 private SignalServiceMessagePipe unidentifiedMessagePipe
= null;
166 public Manager(String username
, String settingsPath
, SignalServiceConfiguration serviceConfiguration
, String userAgent
) {
167 this.username
= username
;
168 this.settingsPath
= settingsPath
;
169 this.dataPath
= this.settingsPath
+ "/data";
170 this.attachmentsPath
= this.settingsPath
+ "/attachments";
171 this.avatarsPath
= this.settingsPath
+ "/avatars";
172 this.serviceConfiguration
= serviceConfiguration
;
173 this.userAgent
= userAgent
;
176 public String
getUsername() {
180 public SignalServiceAddress
getSelfAddress() {
181 return account
.getSelfAddress();
184 private SignalServiceAccountManager
getSignalServiceAccountManager() {
185 return new SignalServiceAccountManager(serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), userAgent
, timer
);
188 private IdentityKey
getIdentity() {
189 return account
.getSignalProtocolStore().getIdentityKeyPair().getPublicKey();
192 public int getDeviceId() {
193 return account
.getDeviceId();
196 private String
getMessageCachePath() {
197 return this.dataPath
+ "/" + username
+ ".d/msg-cache";
200 private String
getMessageCachePath(String sender
) {
201 if (sender
== null || sender
.isEmpty()) {
202 return getMessageCachePath();
205 return getMessageCachePath() + "/" + sender
.replace("/", "_");
208 private File
getMessageCacheFile(String sender
, long now
, long timestamp
) throws IOException
{
209 String cachePath
= getMessageCachePath(sender
);
210 IOUtils
.createPrivateDirectories(cachePath
);
211 return new File(cachePath
+ "/" + now
+ "_" + timestamp
);
214 public boolean userHasKeys() {
215 return account
!= null && account
.getSignalProtocolStore() != null;
218 public void init() throws IOException
{
219 if (!SignalAccount
.userExists(dataPath
, username
)) {
222 account
= SignalAccount
.load(dataPath
, username
);
223 account
.setResolver(this::resolveSignalServiceAddress
);
225 migrateLegacyConfigs();
227 accountManager
= getSignalServiceAccountManager();
228 if (account
.isRegistered()) {
229 if (accountManager
.getPreKeysCount() < ServiceConfig
.PREKEY_MINIMUM_COUNT
) {
233 if (account
.getUuid() == null) {
234 account
.setUuid(accountManager
.getOwnUuid());
240 private void migrateLegacyConfigs() {
241 // Copy group avatars that were previously stored in the attachments folder
242 // to the new avatar folder
243 if (JsonGroupStore
.groupsWithLegacyAvatarId
.size() > 0) {
244 for (GroupInfo g
: JsonGroupStore
.groupsWithLegacyAvatarId
) {
245 File avatarFile
= getGroupAvatarFile(g
.groupId
);
246 File attachmentFile
= getAttachmentFile(new SignalServiceAttachmentRemoteId(g
.getAvatarId()));
247 if (!avatarFile
.exists() && attachmentFile
.exists()) {
249 IOUtils
.createPrivateDirectories(avatarsPath
);
250 Files
.copy(attachmentFile
.toPath(), avatarFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
251 } catch (Exception e
) {
256 JsonGroupStore
.groupsWithLegacyAvatarId
.clear();
259 if (account
.getProfileKey() == null) {
260 // Old config file, creating new profile key
261 account
.setProfileKey(KeyUtils
.createProfileKey());
266 private void createNewIdentity() throws IOException
{
267 IdentityKeyPair identityKey
= KeyHelper
.generateIdentityKeyPair();
268 int registrationId
= KeyHelper
.generateRegistrationId(false);
269 if (username
== null) {
270 account
= SignalAccount
.createTemporaryAccount(identityKey
, registrationId
);
271 account
.setResolver(this::resolveSignalServiceAddress
);
273 ProfileKey profileKey
= KeyUtils
.createProfileKey();
274 account
= SignalAccount
.create(dataPath
, username
, identityKey
, registrationId
, profileKey
);
275 account
.setResolver(this::resolveSignalServiceAddress
);
280 public boolean isRegistered() {
281 return account
!= null && account
.isRegistered();
284 public void register(boolean voiceVerification
) throws IOException
{
285 if (account
== null) {
288 account
.setPassword(KeyUtils
.createPassword());
289 account
.setUuid(null);
290 accountManager
= getSignalServiceAccountManager();
292 if (voiceVerification
) {
293 accountManager
.requestVoiceVerificationCode(Locale
.getDefault(), Optional
.absent(), Optional
.absent());
295 accountManager
.requestSmsVerificationCode(false, Optional
.absent(), Optional
.absent());
298 account
.setRegistered(false);
302 public void updateAccountAttributes() throws IOException
{
303 accountManager
.setAccountAttributes(account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, account
.getRegistrationLockPin(), account
.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, ServiceConfig
.capabilities
);
306 public void setProfileName(String name
) throws IOException
{
307 accountManager
.setProfileName(account
.getProfileKey(), name
);
310 public void setProfileAvatar(File avatar
) throws IOException
{
311 final StreamDetails streamDetails
= Utils
.createStreamDetailsFromFile(avatar
);
312 accountManager
.setProfileAvatar(account
.getProfileKey(), streamDetails
);
313 streamDetails
.getStream().close();
316 public void removeProfileAvatar() throws IOException
{
317 accountManager
.setProfileAvatar(account
.getProfileKey(), null);
320 public void unregister() throws IOException
{
321 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
322 // If this is the master device, other users can't send messages to this number anymore.
323 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
324 accountManager
.setGcmId(Optional
.absent());
326 account
.setRegistered(false);
330 public String
getDeviceLinkUri() throws TimeoutException
, IOException
{
331 if (account
== null) {
334 account
.setPassword(KeyUtils
.createPassword());
335 accountManager
= getSignalServiceAccountManager();
336 String uuid
= accountManager
.getNewDeviceUuid();
338 return Utils
.createDeviceLinkUri(new Utils
.DeviceLinkInfo(uuid
, getIdentity().getPublicKey()));
341 public void finishDeviceLink(String deviceName
) throws IOException
, InvalidKeyException
, TimeoutException
, UserAlreadyExists
{
342 account
.setSignalingKey(KeyUtils
.createSignalingKey());
343 SignalServiceAccountManager
.NewDeviceRegistrationReturn ret
= accountManager
.finishNewDeviceRegistration(account
.getSignalProtocolStore().getIdentityKeyPair(), account
.getSignalingKey(), false, true, account
.getSignalProtocolStore().getLocalRegistrationId(), deviceName
);
345 username
= ret
.getNumber();
346 // TODO do this check before actually registering
347 if (SignalAccount
.userExists(dataPath
, username
)) {
348 throw new UserAlreadyExists(username
, SignalAccount
.getFileName(dataPath
, username
));
351 // Create new account with the synced identity
352 byte[] profileKeyBytes
= ret
.getProfileKey();
353 ProfileKey profileKey
;
354 if (profileKeyBytes
== null) {
355 profileKey
= KeyUtils
.createProfileKey();
358 profileKey
= new ProfileKey(profileKeyBytes
);
359 } catch (InvalidInputException e
) {
360 throw new IOException("Received invalid profileKey", e
);
363 account
= SignalAccount
.createLinkedAccount(dataPath
, username
, ret
.getUuid(), account
.getPassword(), ret
.getDeviceId(), ret
.getIdentity(), account
.getSignalProtocolStore().getLocalRegistrationId(), account
.getSignalingKey(), profileKey
);
364 account
.setResolver(this::resolveSignalServiceAddress
);
369 requestSyncContacts();
370 requestSyncBlocked();
371 requestSyncConfiguration();
376 public List
<DeviceInfo
> getLinkedDevices() throws IOException
{
377 List
<DeviceInfo
> devices
= accountManager
.getDevices();
378 account
.setMultiDevice(devices
.size() > 1);
383 public void removeLinkedDevices(int deviceId
) throws IOException
{
384 accountManager
.removeDevice(deviceId
);
385 List
<DeviceInfo
> devices
= accountManager
.getDevices();
386 account
.setMultiDevice(devices
.size() > 1);
390 public void addDeviceLink(URI linkUri
) throws IOException
, InvalidKeyException
{
391 Utils
.DeviceLinkInfo info
= Utils
.parseDeviceLinkUri(linkUri
);
393 addDevice(info
.deviceIdentifier
, info
.deviceKey
);
396 private void addDevice(String deviceIdentifier
, ECPublicKey deviceKey
) throws IOException
, InvalidKeyException
{
397 IdentityKeyPair identityKeyPair
= account
.getSignalProtocolStore().getIdentityKeyPair();
398 String verificationCode
= accountManager
.getNewDeviceVerificationCode();
400 accountManager
.addDevice(deviceIdentifier
, deviceKey
, identityKeyPair
, Optional
.of(account
.getProfileKey().serialize()), verificationCode
);
401 account
.setMultiDevice(true);
405 private List
<PreKeyRecord
> generatePreKeys() {
406 List
<PreKeyRecord
> records
= new ArrayList
<>(ServiceConfig
.PREKEY_BATCH_SIZE
);
408 final int offset
= account
.getPreKeyIdOffset();
409 for (int i
= 0; i
< ServiceConfig
.PREKEY_BATCH_SIZE
; i
++) {
410 int preKeyId
= (offset
+ i
) % Medium
.MAX_VALUE
;
411 ECKeyPair keyPair
= Curve
.generateKeyPair();
412 PreKeyRecord
record = new PreKeyRecord(preKeyId
, keyPair
);
417 account
.addPreKeys(records
);
423 private SignedPreKeyRecord
generateSignedPreKey(IdentityKeyPair identityKeyPair
) {
425 ECKeyPair keyPair
= Curve
.generateKeyPair();
426 byte[] signature
= Curve
.calculateSignature(identityKeyPair
.getPrivateKey(), keyPair
.getPublicKey().serialize());
427 SignedPreKeyRecord
record = new SignedPreKeyRecord(account
.getNextSignedPreKeyId(), System
.currentTimeMillis(), keyPair
, signature
);
429 account
.addSignedPreKey(record);
433 } catch (InvalidKeyException e
) {
434 throw new AssertionError(e
);
438 public void verifyAccount(String verificationCode
, String pin
) throws IOException
{
439 verificationCode
= verificationCode
.replace("-", "");
440 account
.setSignalingKey(KeyUtils
.createSignalingKey());
441 // TODO make unrestricted unidentified access configurable
442 VerifyAccountResponse response
= accountManager
.verifyAccountWithCode(verificationCode
, account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, pin
, null, getSelfUnidentifiedAccessKey(), false, ServiceConfig
.capabilities
);
444 UUID uuid
= UuidUtil
.parseOrNull(response
.getUuid());
445 // TODO response.isStorageCapable()
446 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
447 account
.setRegistered(true);
448 account
.setUuid(uuid
);
449 account
.setRegistrationLockPin(pin
);
450 account
.getSignalProtocolStore().saveIdentity(account
.getSelfAddress(), account
.getSignalProtocolStore().getIdentityKeyPair().getPublicKey(), TrustLevel
.TRUSTED_VERIFIED
);
456 public void setRegistrationLockPin(Optional
<String
> pin
) throws IOException
{
457 if (pin
.isPresent()) {
458 account
.setRegistrationLockPin(pin
.get());
459 throw new RuntimeException("Not implemented anymore, will be replaced with KBS");
461 account
.setRegistrationLockPin(null);
462 accountManager
.removeRegistrationLockV1();
467 private void refreshPreKeys() throws IOException
{
468 List
<PreKeyRecord
> oneTimePreKeys
= generatePreKeys();
469 final IdentityKeyPair identityKeyPair
= account
.getSignalProtocolStore().getIdentityKeyPair();
470 SignedPreKeyRecord signedPreKeyRecord
= generateSignedPreKey(identityKeyPair
);
472 accountManager
.setPreKeys(getIdentity(), signedPreKeyRecord
, oneTimePreKeys
);
475 private SignalServiceMessageReceiver
getMessageReceiver() {
476 // TODO implement ZkGroup support
477 final ClientZkProfileOperations clientZkProfileOperations
= null;
478 return new SignalServiceMessageReceiver(serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), account
.getSignalingKey(), userAgent
, null, timer
, clientZkProfileOperations
);
481 private SignalServiceMessageSender
getMessageSender() {
482 // TODO implement ZkGroup support
483 final ClientZkProfileOperations clientZkProfileOperations
= null;
484 final boolean attachmentsV3
= false;
485 return new SignalServiceMessageSender(serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(),
486 account
.getDeviceId(), account
.getSignalProtocolStore(), userAgent
, account
.isMultiDevice(), attachmentsV3
, Optional
.fromNullable(messagePipe
), Optional
.fromNullable(unidentifiedMessagePipe
), Optional
.absent(), clientZkProfileOperations
);
489 private SignalServiceProfile
getRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
) throws IOException
{
490 SignalServiceMessagePipe pipe
= unidentifiedMessagePipe
!= null && unidentifiedAccess
.isPresent() ? unidentifiedMessagePipe
495 return pipe
.getProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
496 } catch (IOException ignored
) {
500 SignalServiceMessageReceiver receiver
= getMessageReceiver();
502 return receiver
.retrieveProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
503 } catch (VerificationFailedException e
) {
504 throw new AssertionError(e
);
508 private Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(byte[] groupId
) throws IOException
{
509 File file
= getGroupAvatarFile(groupId
);
510 if (!file
.exists()) {
511 return Optional
.absent();
514 return Optional
.of(Utils
.createAttachment(file
));
517 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(String number
) throws IOException
{
518 File file
= getContactAvatarFile(number
);
519 if (!file
.exists()) {
520 return Optional
.absent();
523 return Optional
.of(Utils
.createAttachment(file
));
526 private GroupInfo
getGroupForSending(byte[] groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
527 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
529 throw new GroupNotFoundException(groupId
);
531 if (!g
.isMember(account
.getSelfAddress())) {
532 throw new NotAGroupMemberException(groupId
, g
.name
);
537 public List
<GroupInfo
> getGroups() {
538 return account
.getGroupStore().getGroups();
542 public long sendGroupMessage(String messageText
, List
<String
> attachments
,
544 throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
545 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
546 if (attachments
!= null) {
547 messageBuilder
.withAttachments(Utils
.getSignalServiceAttachments(attachments
));
549 if (groupId
!= null) {
550 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
553 messageBuilder
.asGroupMessage(group
);
556 final GroupInfo g
= getGroupForSending(groupId
);
558 messageBuilder
.withExpiration(g
.messageExpirationTime
);
560 return sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
563 public void sendGroupMessageReaction(String emoji
, boolean remove
, String targetAuthor
,
564 long targetSentTimestamp
, byte[] groupId
)
565 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
566 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, canonicalizeAndResolveSignalServiceAddress(targetAuthor
), targetSentTimestamp
);
567 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
568 .withReaction(reaction
);
569 if (groupId
!= null) {
570 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
573 messageBuilder
.asGroupMessage(group
);
575 final GroupInfo g
= getGroupForSending(groupId
);
576 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
579 public void sendQuitGroupMessage(byte[] groupId
) throws GroupNotFoundException
, IOException
, EncapsulatedExceptions
{
580 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
584 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
585 .asGroupMessage(group
);
587 final GroupInfo g
= getGroupForSending(groupId
);
588 g
.removeMember(account
.getSelfAddress());
589 account
.getGroupStore().updateGroup(g
);
591 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
594 private byte[] sendUpdateGroupMessage(byte[] groupId
, String name
, Collection
<SignalServiceAddress
> members
, String avatarFile
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
596 if (groupId
== null) {
598 g
= new GroupInfo(KeyUtils
.createGroupId());
599 g
.addMembers(Collections
.singleton(account
.getSelfAddress()));
601 g
= getGroupForSending(groupId
);
608 if (members
!= null) {
609 final Set
<String
> newE164Members
= new HashSet
<>();
610 for (SignalServiceAddress member
: members
) {
611 if (g
.isMember(member
) || !member
.getNumber().isPresent()) {
614 newE164Members
.add(member
.getNumber().get());
617 final List
<ContactTokenDetails
> contacts
= accountManager
.getContacts(newE164Members
);
618 if (contacts
.size() != newE164Members
.size()) {
619 // Some of the new members are not registered on Signal
620 for (ContactTokenDetails contact
: contacts
) {
621 newE164Members
.remove(contact
.getNumber());
623 System
.err
.println("Failed to add members " + Util
.join(", ", newE164Members
) + " to group: Not registered on Signal");
624 System
.err
.println("Aborting…");
628 g
.addMembers(members
);
631 if (avatarFile
!= null) {
632 IOUtils
.createPrivateDirectories(avatarsPath
);
633 File aFile
= getGroupAvatarFile(g
.groupId
);
634 Files
.copy(Paths
.get(avatarFile
), aFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
637 account
.getGroupStore().updateGroup(g
);
639 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
641 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
645 private void sendUpdateGroupMessage(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
646 if (groupId
== null) {
649 GroupInfo g
= getGroupForSending(groupId
);
651 if (!g
.isMember(recipient
)) {
655 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
657 // Send group message only to the recipient who requested it
658 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
661 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfo g
) {
662 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
665 .withMembers(new ArrayList
<>(g
.getMembers()));
667 File aFile
= getGroupAvatarFile(g
.groupId
);
668 if (aFile
.exists()) {
670 group
.withAvatar(Utils
.createAttachment(aFile
));
671 } catch (IOException e
) {
672 throw new AttachmentInvalidException(aFile
.toString(), e
);
676 return SignalServiceDataMessage
.newBuilder()
677 .asGroupMessage(group
.build())
678 .withExpiration(g
.messageExpirationTime
);
681 private void sendGroupInfoRequest(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
682 if (groupId
== null) {
686 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
)
689 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
690 .asGroupMessage(group
.build());
692 // Send group info request message to the recipient who sent us a message with this groupId
693 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
696 private void sendReceipt(SignalServiceAddress remoteAddress
, long messageId
) throws IOException
, UntrustedIdentityException
{
697 SignalServiceReceiptMessage receiptMessage
= new SignalServiceReceiptMessage(SignalServiceReceiptMessage
.Type
.DELIVERY
,
698 Collections
.singletonList(messageId
),
699 System
.currentTimeMillis());
701 getMessageSender().sendReceipt(remoteAddress
, getAccessFor(remoteAddress
), receiptMessage
);
705 public long sendMessage(String message
, List
<String
> attachments
, String recipient
)
706 throws EncapsulatedExceptions
, AttachmentInvalidException
, IOException
, InvalidNumberException
{
707 List
<String
> recipients
= new ArrayList
<>(1);
708 recipients
.add(recipient
);
709 return sendMessage(message
, attachments
, recipients
);
713 public long sendMessage(String messageText
, List
<String
> attachments
,
714 List
<String
> recipients
)
715 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
716 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
717 if (attachments
!= null) {
718 List
<SignalServiceAttachment
> attachmentStreams
= Utils
.getSignalServiceAttachments(attachments
);
720 // Upload attachments here, so we only upload once even for multiple recipients
721 SignalServiceMessageSender messageSender
= getMessageSender();
722 List
<SignalServiceAttachment
> attachmentPointers
= new ArrayList
<>(attachmentStreams
.size());
723 for (SignalServiceAttachment attachment
: attachmentStreams
) {
724 if (attachment
.isStream()) {
725 attachmentPointers
.add(messageSender
.uploadAttachment(attachment
.asStream()));
726 } else if (attachment
.isPointer()) {
727 attachmentPointers
.add(attachment
.asPointer());
731 messageBuilder
.withAttachments(attachmentPointers
);
733 return sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
736 public void sendMessageReaction(String emoji
, boolean remove
, String targetAuthor
,
737 long targetSentTimestamp
, List
<String
> recipients
)
738 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
739 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, canonicalizeAndResolveSignalServiceAddress(targetAuthor
), targetSentTimestamp
);
740 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
741 .withReaction(reaction
);
742 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
746 public void sendEndSessionMessage(List
<String
> recipients
) throws IOException
, EncapsulatedExceptions
, InvalidNumberException
{
747 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
748 .asEndSessionMessage();
750 final Collection
<SignalServiceAddress
> signalServiceAddresses
= getSignalServiceAddresses(recipients
);
752 sendMessageLegacy(messageBuilder
, signalServiceAddresses
);
753 } catch (Exception e
) {
754 for (SignalServiceAddress address
: signalServiceAddresses
) {
755 handleEndSession(address
);
762 public String
getContactName(String number
) throws InvalidNumberException
{
763 ContactInfo contact
= account
.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number
));
764 if (contact
== null) {
772 public void setContactName(String number
, String name
) throws InvalidNumberException
{
773 final SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(number
);
774 ContactInfo contact
= account
.getContactStore().getContact(address
);
775 if (contact
== null) {
776 contact
= new ContactInfo(address
);
777 System
.err
.println("Add contact " + contact
.number
+ " named " + name
);
779 System
.err
.println("Updating contact " + contact
.number
+ " name " + contact
.name
+ " -> " + name
);
782 account
.getContactStore().updateContact(contact
);
787 public void setContactBlocked(String number
, boolean blocked
) throws InvalidNumberException
{
788 setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number
), blocked
);
791 private void setContactBlocked(SignalServiceAddress address
, boolean blocked
) {
792 ContactInfo contact
= account
.getContactStore().getContact(address
);
793 if (contact
== null) {
794 contact
= new ContactInfo(address
);
795 System
.err
.println("Adding and " + (blocked ?
"blocking" : "unblocking") + " contact " + address
.getNumber().orNull());
797 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " contact " + address
.getNumber().orNull());
799 contact
.blocked
= blocked
;
800 account
.getContactStore().updateContact(contact
);
805 public void setGroupBlocked(final byte[] groupId
, final boolean blocked
) throws GroupNotFoundException
{
806 GroupInfo group
= getGroup(groupId
);
808 throw new GroupNotFoundException(groupId
);
810 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " group " + Base64
.encodeBytes(groupId
));
811 group
.blocked
= blocked
;
812 account
.getGroupStore().updateGroup(group
);
818 public List
<byte[]> getGroupIds() {
819 List
<GroupInfo
> groups
= getGroups();
820 List
<byte[]> ids
= new ArrayList
<>(groups
.size());
821 for (GroupInfo group
: groups
) {
822 ids
.add(group
.groupId
);
828 public String
getGroupName(byte[] groupId
) {
829 GroupInfo group
= getGroup(groupId
);
838 public List
<String
> getGroupMembers(byte[] groupId
) {
839 GroupInfo group
= getGroup(groupId
);
841 return Collections
.emptyList();
843 return new ArrayList
<>(group
.getMembersE164());
848 public byte[] updateGroup(byte[] groupId
, String name
, List
<String
> members
, String avatar
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
, InvalidNumberException
{
849 if (groupId
.length
== 0) {
852 if (name
.isEmpty()) {
855 if (members
.size() == 0) {
858 if (avatar
.isEmpty()) {
861 return sendUpdateGroupMessage(groupId
, name
, members
== null ?
null : getSignalServiceAddresses(members
), avatar
);
865 * Change the expiration timer for a contact
867 public void setExpirationTimer(SignalServiceAddress address
, int messageExpirationTimer
) {
868 ContactInfo c
= account
.getContactStore().getContact(address
);
869 c
.messageExpirationTime
= messageExpirationTimer
;
870 account
.getContactStore().updateContact(c
);
874 * Change the expiration timer for a group
876 public void setExpirationTimer(byte[] groupId
, int messageExpirationTimer
) {
877 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
878 g
.messageExpirationTime
= messageExpirationTimer
;
879 account
.getGroupStore().updateGroup(g
);
883 * Upload the sticker pack from path.
885 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
886 * @return if successful, returns the URL to install the sticker pack in the signal app
888 public String
uploadStickerPack(String path
) throws IOException
, StickerPackInvalidException
{
889 SignalServiceStickerManifestUpload manifest
= getSignalServiceStickerManifestUpload(path
);
891 SignalServiceMessageSender messageSender
= getMessageSender();
893 byte[] packKey
= KeyUtils
.createStickerUploadKey();
894 String packId
= messageSender
.uploadStickerManifest(manifest
, packKey
);
897 return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder
.encode(packId
, "utf-8") + "&pack_key=" + URLEncoder
.encode(Hex
.toStringCondensed(packKey
), "utf-8"))
899 } catch (URISyntaxException e
) {
900 throw new AssertionError(e
);
904 private SignalServiceStickerManifestUpload
getSignalServiceStickerManifestUpload(final String path
) throws IOException
, StickerPackInvalidException
{
906 String rootPath
= null;
908 final File file
= new File(path
);
909 if (file
.getName().endsWith(".zip")) {
910 zip
= new ZipFile(file
);
911 } else if (file
.getName().equals("manifest.json")) {
912 rootPath
= file
.getParent();
914 throw new StickerPackInvalidException("Could not find manifest.json");
917 JsonStickerPack pack
= parseStickerPack(rootPath
, zip
);
919 if (pack
.stickers
== null) {
920 throw new StickerPackInvalidException("Must set a 'stickers' field.");
923 if (pack
.stickers
.isEmpty()) {
924 throw new StickerPackInvalidException("Must include stickers.");
927 List
<StickerInfo
> stickers
= new ArrayList
<>(pack
.stickers
.size());
928 for (JsonStickerPack
.JsonSticker sticker
: pack
.stickers
) {
929 if (sticker
.file
== null) {
930 throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
933 Pair
<InputStream
, Long
> data
;
935 data
= getInputStreamAndLength(rootPath
, zip
, sticker
.file
);
936 } catch (IOException ignored
) {
937 throw new StickerPackInvalidException("Could not find find " + sticker
.file
);
940 StickerInfo stickerInfo
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(sticker
.emoji
).or(""));
941 stickers
.add(stickerInfo
);
944 StickerInfo cover
= null;
945 if (pack
.cover
!= null) {
946 if (pack
.cover
.file
== null) {
947 throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
950 Pair
<InputStream
, Long
> data
;
952 data
= getInputStreamAndLength(rootPath
, zip
, pack
.cover
.file
);
953 } catch (IOException ignored
) {
954 throw new StickerPackInvalidException("Could not find find " + pack
.cover
.file
);
957 cover
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(pack
.cover
.emoji
).or(""));
960 return new SignalServiceStickerManifestUpload(
967 private static JsonStickerPack
parseStickerPack(String rootPath
, ZipFile zip
) throws IOException
{
968 InputStream inputStream
;
970 inputStream
= zip
.getInputStream(zip
.getEntry("manifest.json"));
972 inputStream
= new FileInputStream((new File(rootPath
, "manifest.json")));
974 return new ObjectMapper().readValue(inputStream
, JsonStickerPack
.class);
977 private static Pair
<InputStream
, Long
> getInputStreamAndLength(final String rootPath
, final ZipFile zip
, final String subfile
) throws IOException
{
979 final ZipEntry entry
= zip
.getEntry(subfile
);
980 return new Pair
<>(zip
.getInputStream(entry
), entry
.getSize());
982 final File file
= new File(rootPath
, subfile
);
983 return new Pair
<>(new FileInputStream(file
), file
.length());
987 private void requestSyncGroups() throws IOException
{
988 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
).build();
989 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
991 sendSyncMessage(message
);
992 } catch (UntrustedIdentityException e
) {
997 private void requestSyncContacts() throws IOException
{
998 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
).build();
999 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1001 sendSyncMessage(message
);
1002 } catch (UntrustedIdentityException e
) {
1003 e
.printStackTrace();
1007 private void requestSyncBlocked() throws IOException
{
1008 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
).build();
1009 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1011 sendSyncMessage(message
);
1012 } catch (UntrustedIdentityException e
) {
1013 e
.printStackTrace();
1017 private void requestSyncConfiguration() throws IOException
{
1018 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
).build();
1019 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1021 sendSyncMessage(message
);
1022 } catch (UntrustedIdentityException e
) {
1023 e
.printStackTrace();
1027 private byte[] getSenderCertificate() {
1028 // TODO support UUID capable sender certificates
1029 // byte[] certificate = accountManager.getSenderCertificate();
1032 certificate
= accountManager
.getSenderCertificateLegacy();
1033 } catch (IOException e
) {
1034 System
.err
.println("Failed to get sender certificate: " + e
);
1037 // TODO cache for a day
1041 private byte[] getSelfUnidentifiedAccessKey() {
1042 return UnidentifiedAccess
.deriveAccessKeyFrom(account
.getProfileKey());
1045 private static SignalProfile
decryptProfile(SignalServiceProfile encryptedProfile
, ProfileKey profileKey
) throws IOException
{
1046 ProfileCipher profileCipher
= new ProfileCipher(profileKey
);
1048 return new SignalProfile(
1049 encryptedProfile
.getIdentityKey(),
1050 encryptedProfile
.getName() == null ?
null : new String(profileCipher
.decryptName(Base64
.decode(encryptedProfile
.getName()))),
1051 encryptedProfile
.getAvatar(),
1052 encryptedProfile
.getUnidentifiedAccess() == null || !profileCipher
.verifyUnidentifiedAccess(Base64
.decode(encryptedProfile
.getUnidentifiedAccess())) ?
null : encryptedProfile
.getUnidentifiedAccess(),
1053 encryptedProfile
.isUnrestrictedUnidentifiedAccess()
1055 } catch (InvalidCiphertextException e
) {
1060 private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient
) {
1061 ContactInfo contact
= account
.getContactStore().getContact(recipient
);
1062 if (contact
== null || contact
.profileKey
== null) {
1065 ProfileKey theirProfileKey
;
1067 theirProfileKey
= new ProfileKey(Base64
.decode(contact
.profileKey
));
1068 } catch (InvalidInputException
| IOException e
) {
1069 throw new AssertionError(e
);
1071 SignalProfile targetProfile
;
1073 targetProfile
= decryptProfile(getRecipientProfile(recipient
, Optional
.absent()), theirProfileKey
);
1074 } catch (IOException e
) {
1075 System
.err
.println("Failed to get recipient profile: " + e
);
1079 if (targetProfile
== null || targetProfile
.getUnidentifiedAccess() == null) {
1083 if (targetProfile
.isUnrestrictedUnidentifiedAccess()) {
1084 return KeyUtils
.createUnrestrictedUnidentifiedAccess();
1087 return UnidentifiedAccess
.deriveAccessKeyFrom(theirProfileKey
);
1090 private Optional
<UnidentifiedAccessPair
> getAccessForSync() {
1091 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1092 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1094 if (selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1095 return Optional
.absent();
1099 return Optional
.of(new UnidentifiedAccessPair(
1100 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1101 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1103 } catch (InvalidCertificateException e
) {
1104 return Optional
.absent();
1108 private List
<Optional
<UnidentifiedAccessPair
>> getAccessFor(Collection
<SignalServiceAddress
> recipients
) {
1109 List
<Optional
<UnidentifiedAccessPair
>> result
= new ArrayList
<>(recipients
.size());
1110 for (SignalServiceAddress recipient
: recipients
) {
1111 result
.add(getAccessFor(recipient
));
1116 private Optional
<UnidentifiedAccessPair
> getAccessFor(SignalServiceAddress recipient
) {
1117 byte[] recipientUnidentifiedAccessKey
= getTargetUnidentifiedAccessKey(recipient
);
1118 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1119 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1121 if (recipientUnidentifiedAccessKey
== null || selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1122 return Optional
.absent();
1126 return Optional
.of(new UnidentifiedAccessPair(
1127 new UnidentifiedAccess(recipientUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1128 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1130 } catch (InvalidCertificateException e
) {
1131 return Optional
.absent();
1135 private Optional
<UnidentifiedAccess
> getUnidentifiedAccess(SignalServiceAddress recipient
) {
1136 Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1138 if (unidentifiedAccess
.isPresent()) {
1139 return unidentifiedAccess
.get().getTargetUnidentifiedAccess();
1142 return Optional
.absent();
1145 private void sendSyncMessage(SignalServiceSyncMessage message
)
1146 throws IOException
, UntrustedIdentityException
{
1147 SignalServiceMessageSender messageSender
= getMessageSender();
1149 messageSender
.sendMessage(message
, getAccessForSync());
1150 } catch (UntrustedIdentityException e
) {
1151 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1157 * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult.
1159 private long sendMessageLegacy(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1160 throws EncapsulatedExceptions
, IOException
{
1161 final long timestamp
= System
.currentTimeMillis();
1162 messageBuilder
.withTimestamp(timestamp
);
1163 List
<SendMessageResult
> results
= sendMessage(messageBuilder
, recipients
);
1165 List
<UntrustedIdentityException
> untrustedIdentities
= new LinkedList
<>();
1166 List
<UnregisteredUserException
> unregisteredUsers
= new LinkedList
<>();
1167 List
<NetworkFailureException
> networkExceptions
= new LinkedList
<>();
1169 for (SendMessageResult result
: results
) {
1170 if (result
.isUnregisteredFailure()) {
1171 unregisteredUsers
.add(new UnregisteredUserException(result
.getAddress().getLegacyIdentifier(), null));
1172 } else if (result
.isNetworkFailure()) {
1173 networkExceptions
.add(new NetworkFailureException(result
.getAddress().getLegacyIdentifier(), null));
1174 } else if (result
.getIdentityFailure() != null) {
1175 untrustedIdentities
.add(new UntrustedIdentityException("Untrusted", result
.getAddress().getLegacyIdentifier(), result
.getIdentityFailure().getIdentityKey()));
1178 if (!untrustedIdentities
.isEmpty() || !unregisteredUsers
.isEmpty() || !networkExceptions
.isEmpty()) {
1179 throw new EncapsulatedExceptions(untrustedIdentities
, unregisteredUsers
, networkExceptions
);
1184 private Collection
<SignalServiceAddress
> getSignalServiceAddresses(Collection
<String
> numbers
) throws InvalidNumberException
{
1185 final Set
<SignalServiceAddress
> signalServiceAddresses
= new HashSet
<>(numbers
.size());
1187 for (String number
: numbers
) {
1188 signalServiceAddresses
.add(canonicalizeAndResolveSignalServiceAddress(number
));
1190 return signalServiceAddresses
;
1193 private List
<SendMessageResult
> sendMessage(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1194 throws IOException
{
1195 if (messagePipe
== null) {
1196 messagePipe
= getMessageReceiver().createMessagePipe();
1198 if (unidentifiedMessagePipe
== null) {
1199 unidentifiedMessagePipe
= getMessageReceiver().createUnidentifiedMessagePipe();
1201 SignalServiceDataMessage message
= null;
1203 SignalServiceMessageSender messageSender
= getMessageSender();
1205 message
= messageBuilder
.build();
1206 if (message
.getGroupContext().isPresent()) {
1208 final boolean isRecipientUpdate
= false;
1209 List
<SendMessageResult
> result
= messageSender
.sendMessage(new ArrayList
<>(recipients
), getAccessFor(recipients
), isRecipientUpdate
, message
);
1210 for (SendMessageResult r
: result
) {
1211 if (r
.getIdentityFailure() != null) {
1212 account
.getSignalProtocolStore().saveIdentity(r
.getAddress(), r
.getIdentityFailure().getIdentityKey(), TrustLevel
.UNTRUSTED
);
1216 } catch (UntrustedIdentityException e
) {
1217 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1218 return Collections
.emptyList();
1220 } else if (recipients
.size() == 1 && recipients
.contains(account
.getSelfAddress())) {
1221 SignalServiceAddress recipient
= account
.getSelfAddress();
1222 final Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1223 SentTranscriptMessage transcript
= new SentTranscriptMessage(Optional
.of(recipient
),
1224 message
.getTimestamp(),
1226 message
.getExpiresInSeconds(),
1227 Collections
.singletonMap(recipient
, unidentifiedAccess
.isPresent()),
1229 SignalServiceSyncMessage syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
1231 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1233 messageSender
.sendMessage(syncMessage
, unidentifiedAccess
);
1234 } catch (UntrustedIdentityException e
) {
1235 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1236 results
.add(SendMessageResult
.identityFailure(recipient
, e
.getIdentityKey()));
1240 // Send to all individually, so sync messages are sent correctly
1241 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1242 for (SignalServiceAddress address
: recipients
) {
1243 ContactInfo contact
= account
.getContactStore().getContact(address
);
1244 if (contact
!= null) {
1245 messageBuilder
.withExpiration(contact
.messageExpirationTime
);
1246 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
1248 messageBuilder
.withExpiration(0);
1249 messageBuilder
.withProfileKey(null);
1251 message
= messageBuilder
.build();
1253 SendMessageResult result
= messageSender
.sendMessage(address
, getAccessFor(address
), message
);
1254 results
.add(result
);
1255 } catch (UntrustedIdentityException e
) {
1256 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1257 results
.add(SendMessageResult
.identityFailure(address
, e
.getIdentityKey()));
1263 if (message
!= null && message
.isEndSession()) {
1264 for (SignalServiceAddress recipient
: recipients
) {
1265 handleEndSession(recipient
);
1272 private SignalServiceContent
decryptMessage(SignalServiceEnvelope envelope
) throws InvalidMetadataMessageException
, ProtocolInvalidMessageException
, ProtocolDuplicateMessageException
, ProtocolLegacyMessageException
, ProtocolInvalidKeyIdException
, InvalidMetadataVersionException
, ProtocolInvalidVersionException
, ProtocolNoSessionException
, ProtocolInvalidKeyException
, SelfSendException
, UnsupportedDataMessageException
, org
.whispersystems
.libsignal
.UntrustedIdentityException
{
1273 SignalServiceCipher cipher
= new SignalServiceCipher(account
.getSelfAddress(), account
.getSignalProtocolStore(), Utils
.getCertificateValidator());
1275 return cipher
.decrypt(envelope
);
1276 } catch (ProtocolUntrustedIdentityException e
) {
1277 if (e
.getCause() instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
) {
1278 org
.whispersystems
.libsignal
.UntrustedIdentityException identityException
= (org
.whispersystems
.libsignal
.UntrustedIdentityException
) e
.getCause();
1279 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(identityException
.getName()), identityException
.getUntrustedIdentity(), TrustLevel
.UNTRUSTED
);
1280 throw identityException
;
1282 throw new AssertionError(e
);
1286 private void handleEndSession(SignalServiceAddress source
) {
1287 account
.getSignalProtocolStore().deleteAllSessions(source
);
1290 private void handleSignalServiceDataMessage(SignalServiceDataMessage message
, boolean isSync
, SignalServiceAddress source
, SignalServiceAddress destination
, boolean ignoreAttachments
) {
1291 if (message
.getGroupContext().isPresent() && message
.getGroupContext().get().getGroupV1().isPresent()) {
1292 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1293 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1294 switch (groupInfo
.getType()) {
1296 if (group
== null) {
1297 group
= new GroupInfo(groupInfo
.getGroupId());
1300 if (groupInfo
.getAvatar().isPresent()) {
1301 SignalServiceAttachment avatar
= groupInfo
.getAvatar().get();
1302 if (avatar
.isPointer()) {
1304 retrieveGroupAvatarAttachment(avatar
.asPointer(), group
.groupId
);
1305 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1306 System
.err
.println("Failed to retrieve group avatar (" + avatar
.asPointer().getRemoteId() + "): " + e
.getMessage());
1311 if (groupInfo
.getName().isPresent()) {
1312 group
.name
= groupInfo
.getName().get();
1315 if (groupInfo
.getMembers().isPresent()) {
1316 group
.addMembers(groupInfo
.getMembers().get()
1318 .map(this::resolveSignalServiceAddress
)
1319 .collect(Collectors
.toSet()));
1322 account
.getGroupStore().updateGroup(group
);
1325 if (group
== null) {
1327 sendGroupInfoRequest(groupInfo
.getGroupId(), source
);
1328 } catch (IOException
| EncapsulatedExceptions e
) {
1329 e
.printStackTrace();
1334 if (group
!= null) {
1335 group
.removeMember(source
);
1336 account
.getGroupStore().updateGroup(group
);
1340 if (group
!= null) {
1342 sendUpdateGroupMessage(groupInfo
.getGroupId(), source
);
1343 } catch (IOException
| EncapsulatedExceptions e
) {
1344 e
.printStackTrace();
1345 } catch (NotAGroupMemberException e
) {
1346 // We have left this group, so don't send a group update message
1352 final SignalServiceAddress conversationPartnerAddress
= isSync ? destination
: source
;
1353 if (message
.isEndSession()) {
1354 handleEndSession(conversationPartnerAddress
);
1356 if (message
.isExpirationUpdate() || message
.getBody().isPresent()) {
1357 if (message
.getGroupContext().isPresent() && message
.getGroupContext().get().getGroupV1().isPresent()) {
1358 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1359 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1360 if (group
== null) {
1361 group
= new GroupInfo(groupInfo
.getGroupId());
1363 if (group
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1364 group
.messageExpirationTime
= message
.getExpiresInSeconds();
1365 account
.getGroupStore().updateGroup(group
);
1368 ContactInfo contact
= account
.getContactStore().getContact(conversationPartnerAddress
);
1369 if (contact
== null) {
1370 contact
= new ContactInfo(conversationPartnerAddress
);
1372 if (contact
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1373 contact
.messageExpirationTime
= message
.getExpiresInSeconds();
1374 account
.getContactStore().updateContact(contact
);
1378 if (message
.getAttachments().isPresent() && !ignoreAttachments
) {
1379 for (SignalServiceAttachment attachment
: message
.getAttachments().get()) {
1380 if (attachment
.isPointer()) {
1382 retrieveAttachment(attachment
.asPointer());
1383 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1384 System
.err
.println("Failed to retrieve attachment (" + attachment
.asPointer().getRemoteId() + "): " + e
.getMessage());
1389 if (message
.getProfileKey().isPresent() && message
.getProfileKey().get().length
== 32) {
1390 if (source
.matches(account
.getSelfAddress())) {
1392 this.account
.setProfileKey(new ProfileKey(message
.getProfileKey().get()));
1393 } catch (InvalidInputException ignored
) {
1395 ContactInfo contact
= account
.getContactStore().getContact(source
);
1396 if (contact
!= null) {
1397 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1398 account
.getContactStore().updateContact(contact
);
1401 ContactInfo contact
= account
.getContactStore().getContact(source
);
1402 if (contact
== null) {
1403 contact
= new ContactInfo(source
);
1405 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1406 account
.getContactStore().updateContact(contact
);
1409 if (message
.getPreviews().isPresent()) {
1410 final List
<SignalServiceDataMessage
.Preview
> previews
= message
.getPreviews().get();
1411 for (SignalServiceDataMessage
.Preview preview
: previews
) {
1412 if (preview
.getImage().isPresent() && preview
.getImage().get().isPointer()) {
1413 SignalServiceAttachmentPointer attachment
= preview
.getImage().get().asPointer();
1415 retrieveAttachment(attachment
);
1416 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1417 System
.err
.println("Failed to retrieve attachment (" + attachment
.getRemoteId() + "): " + e
.getMessage());
1424 private void retryFailedReceivedMessages(ReceiveMessageHandler handler
, boolean ignoreAttachments
) {
1425 final File cachePath
= new File(getMessageCachePath());
1426 if (!cachePath
.exists()) {
1429 for (final File dir
: Objects
.requireNonNull(cachePath
.listFiles())) {
1430 if (!dir
.isDirectory()) {
1431 retryFailedReceivedMessage(handler
, ignoreAttachments
, dir
);
1435 for (final File fileEntry
: Objects
.requireNonNull(dir
.listFiles())) {
1436 if (!fileEntry
.isFile()) {
1439 retryFailedReceivedMessage(handler
, ignoreAttachments
, fileEntry
);
1441 // Try to delete directory if empty
1446 private void retryFailedReceivedMessage(final ReceiveMessageHandler handler
, final boolean ignoreAttachments
, final File fileEntry
) {
1447 SignalServiceEnvelope envelope
;
1449 envelope
= Utils
.loadEnvelope(fileEntry
);
1450 if (envelope
== null) {
1453 } catch (IOException e
) {
1454 e
.printStackTrace();
1457 SignalServiceContent content
= null;
1458 if (!envelope
.isReceipt()) {
1460 content
= decryptMessage(envelope
);
1461 } catch (Exception e
) {
1464 handleMessage(envelope
, content
, ignoreAttachments
);
1467 handler
.handleMessage(envelope
, content
, null);
1469 Files
.delete(fileEntry
.toPath());
1470 } catch (IOException e
) {
1471 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1475 public void receiveMessages(long timeout
, TimeUnit unit
, boolean returnOnTimeout
, boolean ignoreAttachments
, ReceiveMessageHandler handler
) throws IOException
{
1476 retryFailedReceivedMessages(handler
, ignoreAttachments
);
1477 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1480 if (messagePipe
== null) {
1481 messagePipe
= messageReceiver
.createMessagePipe();
1485 SignalServiceEnvelope envelope
;
1486 SignalServiceContent content
= null;
1487 Exception exception
= null;
1488 final long now
= new Date().getTime();
1490 envelope
= messagePipe
.read(timeout
, unit
, envelope1
-> {
1491 // store message on disk, before acknowledging receipt to the server
1493 String source
= envelope1
.getSourceE164().isPresent() ? envelope1
.getSourceE164().get() : "";
1494 File cacheFile
= getMessageCacheFile(source
, now
, envelope1
.getTimestamp());
1495 Utils
.storeEnvelope(envelope1
, cacheFile
);
1496 } catch (IOException e
) {
1497 System
.err
.println("Failed to store encrypted message in disk cache, ignoring: " + e
.getMessage());
1500 } catch (TimeoutException e
) {
1501 if (returnOnTimeout
)
1504 } catch (InvalidVersionException e
) {
1505 System
.err
.println("Ignoring error: " + e
.getMessage());
1508 if (!envelope
.isReceipt()) {
1510 content
= decryptMessage(envelope
);
1511 } catch (Exception e
) {
1514 handleMessage(envelope
, content
, ignoreAttachments
);
1517 if (!isMessageBlocked(envelope
, content
)) {
1518 handler
.handleMessage(envelope
, content
, exception
);
1520 if (!(exception
instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
)) {
1521 File cacheFile
= null;
1523 cacheFile
= getMessageCacheFile(envelope
.getSourceE164().get(), now
, envelope
.getTimestamp());
1524 Files
.delete(cacheFile
.toPath());
1525 // Try to delete directory if empty
1526 new File(getMessageCachePath()).delete();
1527 } catch (IOException e
) {
1528 System
.err
.println("Failed to delete cached message file “" + cacheFile
+ "”: " + e
.getMessage());
1533 if (messagePipe
!= null) {
1534 messagePipe
.shutdown();
1540 private boolean isMessageBlocked(SignalServiceEnvelope envelope
, SignalServiceContent content
) {
1541 SignalServiceAddress source
;
1542 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1543 source
= envelope
.getSourceAddress();
1544 } else if (content
!= null) {
1545 source
= content
.getSender();
1549 ContactInfo sourceContact
= account
.getContactStore().getContact(source
);
1550 if (sourceContact
!= null && sourceContact
.blocked
) {
1554 if (content
!= null && content
.getDataMessage().isPresent()) {
1555 SignalServiceDataMessage message
= content
.getDataMessage().get();
1556 if (message
.getGroupContext().isPresent() && message
.getGroupContext().get().getGroupV1().isPresent()) {
1557 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1558 GroupInfo group
= getGroup(groupInfo
.getGroupId());
1559 if (groupInfo
.getType() == SignalServiceGroup
.Type
.DELIVER
&& group
!= null && group
.blocked
) {
1567 private void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent content
, boolean ignoreAttachments
) {
1568 if (content
!= null) {
1569 SignalServiceAddress sender
;
1570 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1571 sender
= envelope
.getSourceAddress();
1573 sender
= content
.getSender();
1575 if (content
.getDataMessage().isPresent()) {
1576 SignalServiceDataMessage message
= content
.getDataMessage().get();
1578 if (content
.isNeedsReceipt()) {
1580 sendReceipt(sender
, message
.getTimestamp());
1581 } catch (IOException
| UntrustedIdentityException
| IllegalArgumentException e
) {
1582 e
.printStackTrace();
1586 handleSignalServiceDataMessage(message
, false, sender
, account
.getSelfAddress(), ignoreAttachments
);
1588 if (content
.getSyncMessage().isPresent()) {
1589 account
.setMultiDevice(true);
1590 SignalServiceSyncMessage syncMessage
= content
.getSyncMessage().get();
1591 if (syncMessage
.getSent().isPresent()) {
1592 SentTranscriptMessage message
= syncMessage
.getSent().get();
1593 handleSignalServiceDataMessage(message
.getMessage(), true, sender
, message
.getDestination().orNull(), ignoreAttachments
);
1595 if (syncMessage
.getRequest().isPresent()) {
1596 RequestMessage rm
= syncMessage
.getRequest().get();
1597 if (rm
.isContactsRequest()) {
1600 } catch (UntrustedIdentityException
| IOException
| IllegalArgumentException e
) {
1601 e
.printStackTrace();
1604 if (rm
.isGroupsRequest()) {
1607 } catch (UntrustedIdentityException
| IOException
| IllegalArgumentException e
) {
1608 e
.printStackTrace();
1611 if (rm
.isBlockedListRequest()) {
1614 } catch (UntrustedIdentityException
| IOException
| IllegalArgumentException e
) {
1615 e
.printStackTrace();
1618 // TODO Handle rm.isConfigurationRequest();
1620 if (syncMessage
.getGroups().isPresent()) {
1621 File tmpFile
= null;
1623 tmpFile
= IOUtils
.createTempFile();
1624 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(syncMessage
.getGroups().get().asPointer(), tmpFile
)) {
1625 DeviceGroupsInputStream s
= new DeviceGroupsInputStream(attachmentAsStream
);
1627 while ((g
= s
.read()) != null) {
1628 GroupInfo syncGroup
= account
.getGroupStore().getGroup(g
.getId());
1629 if (syncGroup
== null) {
1630 syncGroup
= new GroupInfo(g
.getId());
1632 if (g
.getName().isPresent()) {
1633 syncGroup
.name
= g
.getName().get();
1635 syncGroup
.addMembers(g
.getMembers()
1637 .map(this::resolveSignalServiceAddress
)
1638 .collect(Collectors
.toSet()));
1639 if (!g
.isActive()) {
1640 syncGroup
.removeMember(account
.getSelfAddress());
1642 // Add ourself to the member set as it's marked as active
1643 syncGroup
.addMembers(Collections
.singleton(account
.getSelfAddress()));
1645 syncGroup
.blocked
= g
.isBlocked();
1646 if (g
.getColor().isPresent()) {
1647 syncGroup
.color
= g
.getColor().get();
1650 if (g
.getAvatar().isPresent()) {
1651 retrieveGroupAvatarAttachment(g
.getAvatar().get(), syncGroup
.groupId
);
1653 syncGroup
.inboxPosition
= g
.getInboxPosition().orNull();
1654 syncGroup
.archived
= g
.isArchived();
1655 account
.getGroupStore().updateGroup(syncGroup
);
1658 } catch (Exception e
) {
1659 e
.printStackTrace();
1661 if (tmpFile
!= null) {
1663 Files
.delete(tmpFile
.toPath());
1664 } catch (IOException e
) {
1665 System
.err
.println("Failed to delete received groups temp file “" + tmpFile
+ "”: " + e
.getMessage());
1670 if (syncMessage
.getBlockedList().isPresent()) {
1671 final BlockedListMessage blockedListMessage
= syncMessage
.getBlockedList().get();
1672 for (SignalServiceAddress address
: blockedListMessage
.getAddresses()) {
1673 setContactBlocked(resolveSignalServiceAddress(address
), true);
1675 for (byte[] groupId
: blockedListMessage
.getGroupIds()) {
1677 setGroupBlocked(groupId
, true);
1678 } catch (GroupNotFoundException e
) {
1679 System
.err
.println("BlockedListMessage contained groupID that was not found in GroupStore: " + Base64
.encodeBytes(groupId
));
1683 if (syncMessage
.getContacts().isPresent()) {
1684 File tmpFile
= null;
1686 tmpFile
= IOUtils
.createTempFile();
1687 final ContactsMessage contactsMessage
= syncMessage
.getContacts().get();
1688 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(contactsMessage
.getContactsStream().asPointer(), tmpFile
)) {
1689 DeviceContactsInputStream s
= new DeviceContactsInputStream(attachmentAsStream
);
1690 if (contactsMessage
.isComplete()) {
1691 account
.getContactStore().clear();
1694 while ((c
= s
.read()) != null) {
1695 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
1696 account
.setProfileKey(c
.getProfileKey().get());
1698 final SignalServiceAddress address
= resolveSignalServiceAddress(c
.getAddress());
1699 ContactInfo contact
= account
.getContactStore().getContact(address
);
1700 if (contact
== null) {
1701 contact
= new ContactInfo(address
);
1703 if (c
.getName().isPresent()) {
1704 contact
.name
= c
.getName().get();
1706 if (c
.getColor().isPresent()) {
1707 contact
.color
= c
.getColor().get();
1709 if (c
.getProfileKey().isPresent()) {
1710 contact
.profileKey
= Base64
.encodeBytes(c
.getProfileKey().get().serialize());
1712 if (c
.getVerified().isPresent()) {
1713 final VerifiedMessage verifiedMessage
= c
.getVerified().get();
1714 account
.getSignalProtocolStore().setIdentityTrustLevel(verifiedMessage
.getDestination(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1716 if (c
.getExpirationTimer().isPresent()) {
1717 contact
.messageExpirationTime
= c
.getExpirationTimer().get();
1719 contact
.blocked
= c
.isBlocked();
1720 contact
.inboxPosition
= c
.getInboxPosition().orNull();
1721 contact
.archived
= c
.isArchived();
1722 account
.getContactStore().updateContact(contact
);
1724 if (c
.getAvatar().isPresent()) {
1725 retrieveContactAvatarAttachment(c
.getAvatar().get(), contact
.number
);
1729 } catch (Exception e
) {
1730 e
.printStackTrace();
1732 if (tmpFile
!= null) {
1734 Files
.delete(tmpFile
.toPath());
1735 } catch (IOException e
) {
1736 System
.err
.println("Failed to delete received contacts temp file “" + tmpFile
+ "”: " + e
.getMessage());
1741 if (syncMessage
.getVerified().isPresent()) {
1742 final VerifiedMessage verifiedMessage
= syncMessage
.getVerified().get();
1743 account
.getSignalProtocolStore().setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage
.getDestination()), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1745 if (syncMessage
.getConfiguration().isPresent()) {
1752 private File
getContactAvatarFile(String number
) {
1753 return new File(avatarsPath
, "contact-" + number
);
1756 private File
retrieveContactAvatarAttachment(SignalServiceAttachment attachment
, String number
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1757 IOUtils
.createPrivateDirectories(avatarsPath
);
1758 if (attachment
.isPointer()) {
1759 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1760 return retrieveAttachment(pointer
, getContactAvatarFile(number
), false);
1762 SignalServiceAttachmentStream stream
= attachment
.asStream();
1763 return Utils
.retrieveAttachment(stream
, getContactAvatarFile(number
));
1767 private File
getGroupAvatarFile(byte[] groupId
) {
1768 return new File(avatarsPath
, "group-" + Base64
.encodeBytes(groupId
).replace("/", "_"));
1771 private File
retrieveGroupAvatarAttachment(SignalServiceAttachment attachment
, byte[] groupId
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1772 IOUtils
.createPrivateDirectories(avatarsPath
);
1773 if (attachment
.isPointer()) {
1774 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1775 return retrieveAttachment(pointer
, getGroupAvatarFile(groupId
), false);
1777 SignalServiceAttachmentStream stream
= attachment
.asStream();
1778 return Utils
.retrieveAttachment(stream
, getGroupAvatarFile(groupId
));
1782 public File
getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId
) {
1783 return new File(attachmentsPath
, attachmentId
.toString());
1786 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1787 IOUtils
.createPrivateDirectories(attachmentsPath
);
1788 return retrieveAttachment(pointer
, getAttachmentFile(pointer
.getRemoteId()), true);
1791 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
, File outputFile
, boolean storePreview
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1792 if (storePreview
&& pointer
.getPreview().isPresent()) {
1793 File previewFile
= new File(outputFile
+ ".preview");
1794 try (OutputStream output
= new FileOutputStream(previewFile
)) {
1795 byte[] preview
= pointer
.getPreview().get();
1796 output
.write(preview
, 0, preview
.length
);
1797 } catch (FileNotFoundException e
) {
1798 e
.printStackTrace();
1803 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1805 File tmpFile
= IOUtils
.createTempFile();
1806 try (InputStream input
= messageReceiver
.retrieveAttachment(pointer
, tmpFile
, ServiceConfig
.MAX_ATTACHMENT_SIZE
)) {
1807 try (OutputStream output
= new FileOutputStream(outputFile
)) {
1808 byte[] buffer
= new byte[4096];
1811 while ((read
= input
.read(buffer
)) != -1) {
1812 output
.write(buffer
, 0, read
);
1814 } catch (FileNotFoundException e
) {
1815 e
.printStackTrace();
1820 Files
.delete(tmpFile
.toPath());
1821 } catch (IOException e
) {
1822 System
.err
.println("Failed to delete received attachment temp file “" + tmpFile
+ "”: " + e
.getMessage());
1828 private InputStream
retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer
, File tmpFile
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1829 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1830 return messageReceiver
.retrieveAttachment(pointer
, tmpFile
, ServiceConfig
.MAX_ATTACHMENT_SIZE
);
1834 public boolean isRemote() {
1839 public String
getObjectPath() {
1843 private void sendGroups() throws IOException
, UntrustedIdentityException
{
1844 File groupsFile
= IOUtils
.createTempFile();
1847 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
1848 DeviceGroupsOutputStream out
= new DeviceGroupsOutputStream(fos
);
1849 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1850 out
.write(new DeviceGroup(record.groupId
, Optional
.fromNullable(record.name
),
1851 new ArrayList
<>(record.getMembers()), createGroupAvatarAttachment(record.groupId
),
1852 record.isMember(account
.getSelfAddress()), Optional
.of(record.messageExpirationTime
),
1853 Optional
.fromNullable(record.color
), record.blocked
, Optional
.fromNullable(record.inboxPosition
), record.archived
));
1857 if (groupsFile
.exists() && groupsFile
.length() > 0) {
1858 try (FileInputStream groupsFileStream
= new FileInputStream(groupsFile
)) {
1859 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1860 .withStream(groupsFileStream
)
1861 .withContentType("application/octet-stream")
1862 .withLength(groupsFile
.length())
1865 sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
1870 Files
.delete(groupsFile
.toPath());
1871 } catch (IOException e
) {
1872 System
.err
.println("Failed to delete groups temp file “" + groupsFile
+ "”: " + e
.getMessage());
1877 public void sendContacts() throws IOException
, UntrustedIdentityException
{
1878 File contactsFile
= IOUtils
.createTempFile();
1881 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
1882 DeviceContactsOutputStream out
= new DeviceContactsOutputStream(fos
);
1883 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1884 VerifiedMessage verifiedMessage
= null;
1885 JsonIdentityKeyStore
.Identity currentIdentity
= account
.getSignalProtocolStore().getIdentity(record.getAddress());
1886 if (currentIdentity
!= null) {
1887 verifiedMessage
= new VerifiedMessage(record.getAddress(), currentIdentity
.getIdentityKey(), currentIdentity
.getTrustLevel().toVerifiedState(), currentIdentity
.getDateAdded().getTime());
1890 ProfileKey profileKey
= null;
1892 profileKey
= record.profileKey
== null ?
null : new ProfileKey(Base64
.decode(record.profileKey
));
1893 } catch (InvalidInputException ignored
) {
1895 out
.write(new DeviceContact(record.getAddress(), Optional
.fromNullable(record.name
),
1896 createContactAvatarAttachment(record.number
), Optional
.fromNullable(record.color
),
1897 Optional
.fromNullable(verifiedMessage
), Optional
.fromNullable(profileKey
), record.blocked
,
1898 Optional
.of(record.messageExpirationTime
),
1899 Optional
.fromNullable(record.inboxPosition
), record.archived
));
1902 if (account
.getProfileKey() != null) {
1903 // Send our own profile key as well
1904 out
.write(new DeviceContact(account
.getSelfAddress(),
1905 Optional
.absent(), Optional
.absent(),
1906 Optional
.absent(), Optional
.absent(),
1907 Optional
.of(account
.getProfileKey()),
1908 false, Optional
.absent(), Optional
.absent(), false));
1912 if (contactsFile
.exists() && contactsFile
.length() > 0) {
1913 try (FileInputStream contactsFileStream
= new FileInputStream(contactsFile
)) {
1914 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1915 .withStream(contactsFileStream
)
1916 .withContentType("application/octet-stream")
1917 .withLength(contactsFile
.length())
1920 sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
, true)));
1925 Files
.delete(contactsFile
.toPath());
1926 } catch (IOException e
) {
1927 System
.err
.println("Failed to delete contacts temp file “" + contactsFile
+ "”: " + e
.getMessage());
1932 private void sendBlockedList() throws IOException
, UntrustedIdentityException
{
1933 List
<SignalServiceAddress
> addresses
= new ArrayList
<>();
1934 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1935 if (record.blocked
) {
1936 addresses
.add(record.getAddress());
1939 List
<byte[]> groupIds
= new ArrayList
<>();
1940 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1941 if (record.blocked
) {
1942 groupIds
.add(record.groupId
);
1945 sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
1948 private void sendVerifiedMessage(SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
) throws IOException
, UntrustedIdentityException
{
1949 VerifiedMessage verifiedMessage
= new VerifiedMessage(destination
, identityKey
, trustLevel
.toVerifiedState(), System
.currentTimeMillis());
1950 sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
1953 public List
<ContactInfo
> getContacts() {
1954 return account
.getContactStore().getContacts();
1957 public ContactInfo
getContact(String number
) {
1958 return account
.getContactStore().getContact(Util
.getSignalServiceAddressFromIdentifier(number
));
1961 public GroupInfo
getGroup(byte[] groupId
) {
1962 return account
.getGroupStore().getGroup(groupId
);
1965 public List
<JsonIdentityKeyStore
.Identity
> getIdentities() {
1966 return account
.getSignalProtocolStore().getIdentities();
1969 public List
<JsonIdentityKeyStore
.Identity
> getIdentities(String number
) throws InvalidNumberException
{
1970 return account
.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number
));
1974 * Trust this the identity with this fingerprint
1976 * @param name username of the identity
1977 * @param fingerprint Fingerprint
1979 public boolean trustIdentityVerified(String name
, byte[] fingerprint
) throws InvalidNumberException
{
1980 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
1981 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
1985 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1986 if (!Arrays
.equals(id
.getIdentityKey().serialize(), fingerprint
)) {
1990 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1992 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1993 } catch (IOException
| UntrustedIdentityException e
) {
1994 e
.printStackTrace();
2003 * Trust this the identity with this safety number
2005 * @param name username of the identity
2006 * @param safetyNumber Safety number
2008 public boolean trustIdentityVerifiedSafetyNumber(String name
, String safetyNumber
) throws InvalidNumberException
{
2009 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
2010 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2014 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2015 if (!safetyNumber
.equals(computeSafetyNumber(address
, id
.getIdentityKey()))) {
2019 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2021 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2022 } catch (IOException
| UntrustedIdentityException e
) {
2023 e
.printStackTrace();
2032 * Trust all keys of this identity without verification
2034 * @param name username of the identity
2036 public boolean trustIdentityAllKeys(String name
) {
2037 SignalServiceAddress address
= resolveSignalServiceAddress(name
);
2038 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2042 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2043 if (id
.getTrustLevel() == TrustLevel
.UNTRUSTED
) {
2044 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2046 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2047 } catch (IOException
| UntrustedIdentityException e
) {
2048 e
.printStackTrace();
2056 public String
computeSafetyNumber(SignalServiceAddress theirAddress
, IdentityKey theirIdentityKey
) {
2057 return Utils
.computeSafetyNumber(account
.getSelfAddress(), getIdentity(), theirAddress
, theirIdentityKey
);
2060 public SignalServiceAddress
canonicalizeAndResolveSignalServiceAddress(String identifier
) throws InvalidNumberException
{
2061 String canonicalizedNumber
= UuidUtil
.isUuid(identifier
) ? identifier
: Util
.canonicalizeNumber(identifier
, account
.getUsername());
2062 return resolveSignalServiceAddress(canonicalizedNumber
);
2065 public SignalServiceAddress
resolveSignalServiceAddress(String identifier
) {
2066 SignalServiceAddress address
= Util
.getSignalServiceAddressFromIdentifier(identifier
);
2068 return resolveSignalServiceAddress(address
);
2071 public SignalServiceAddress
resolveSignalServiceAddress(SignalServiceAddress address
) {
2072 if (address
.matches(account
.getSelfAddress())) {
2073 return account
.getSelfAddress();
2076 return account
.getRecipientStore().resolveServiceAddress(address
);
2079 public interface ReceiveMessageHandler
{
2081 void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent decryptedContent
, Throwable e
);