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
.push
.SignalServiceProtos
;
113 import org
.whispersystems
.signalservice
.internal
.push
.UnsupportedDataMessageException
;
114 import org
.whispersystems
.signalservice
.internal
.push
.VerifyAccountResponse
;
115 import org
.whispersystems
.signalservice
.internal
.util
.Hex
;
116 import org
.whispersystems
.util
.Base64
;
119 import java
.io
.FileInputStream
;
120 import java
.io
.FileNotFoundException
;
121 import java
.io
.FileOutputStream
;
122 import java
.io
.IOException
;
123 import java
.io
.InputStream
;
124 import java
.io
.OutputStream
;
126 import java
.net
.URISyntaxException
;
127 import java
.net
.URLEncoder
;
128 import java
.nio
.file
.Files
;
129 import java
.nio
.file
.Paths
;
130 import java
.nio
.file
.StandardCopyOption
;
131 import java
.util
.ArrayDeque
;
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
.Deque
;
138 import java
.util
.HashSet
;
139 import java
.util
.LinkedList
;
140 import java
.util
.List
;
141 import java
.util
.Locale
;
142 import java
.util
.Objects
;
143 import java
.util
.Set
;
144 import java
.util
.UUID
;
145 import java
.util
.concurrent
.TimeUnit
;
146 import java
.util
.concurrent
.TimeoutException
;
147 import java
.util
.stream
.Collectors
;
148 import java
.util
.zip
.ZipEntry
;
149 import java
.util
.zip
.ZipFile
;
151 public class Manager
implements Signal
{
153 private final String settingsPath
;
154 private final String dataPath
;
155 private final String attachmentsPath
;
156 private final String avatarsPath
;
157 private final SleepTimer timer
= new UptimeSleepTimer();
159 private SignalAccount account
;
160 private String username
;
161 private SignalServiceAccountManager accountManager
;
162 private SignalServiceMessagePipe messagePipe
= null;
163 private SignalServiceMessagePipe unidentifiedMessagePipe
= null;
165 public Manager(String username
, String settingsPath
) {
166 this.username
= username
;
167 this.settingsPath
= settingsPath
;
168 this.dataPath
= this.settingsPath
+ "/data";
169 this.attachmentsPath
= this.settingsPath
+ "/attachments";
170 this.avatarsPath
= this.settingsPath
+ "/avatars";
174 public String
getUsername() {
178 public SignalServiceAddress
getSelfAddress() {
179 return account
.getSelfAddress();
182 private SignalServiceAccountManager
getSignalServiceAccountManager() {
183 return new SignalServiceAccountManager(BaseConfig
.serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), BaseConfig
.USER_AGENT
, timer
);
186 private IdentityKey
getIdentity() {
187 return account
.getSignalProtocolStore().getIdentityKeyPair().getPublicKey();
190 public int getDeviceId() {
191 return account
.getDeviceId();
194 private String
getMessageCachePath() {
195 return this.dataPath
+ "/" + username
+ ".d/msg-cache";
198 private String
getMessageCachePath(String sender
) {
199 if (sender
== null || sender
.isEmpty()) {
200 return getMessageCachePath();
203 return getMessageCachePath() + "/" + sender
.replace("/", "_");
206 private File
getMessageCacheFile(String sender
, long now
, long timestamp
) throws IOException
{
207 String cachePath
= getMessageCachePath(sender
);
208 IOUtils
.createPrivateDirectories(cachePath
);
209 return new File(cachePath
+ "/" + now
+ "_" + timestamp
);
212 public boolean userHasKeys() {
213 return account
!= null && account
.getSignalProtocolStore() != null;
216 public void init() throws IOException
{
217 if (!SignalAccount
.userExists(dataPath
, username
)) {
220 account
= SignalAccount
.load(dataPath
, username
);
221 account
.setResolver(this::resolveSignalServiceAddress
);
223 migrateLegacyConfigs();
225 accountManager
= getSignalServiceAccountManager();
226 if (account
.isRegistered()) {
227 if (accountManager
.getPreKeysCount() < BaseConfig
.PREKEY_MINIMUM_COUNT
) {
231 if (account
.getUuid() == null) {
232 account
.setUuid(accountManager
.getOwnUuid());
238 private void migrateLegacyConfigs() {
239 // Copy group avatars that were previously stored in the attachments folder
240 // to the new avatar folder
241 if (JsonGroupStore
.groupsWithLegacyAvatarId
.size() > 0) {
242 for (GroupInfo g
: JsonGroupStore
.groupsWithLegacyAvatarId
) {
243 File avatarFile
= getGroupAvatarFile(g
.groupId
);
244 File attachmentFile
= getAttachmentFile(new SignalServiceAttachmentRemoteId(g
.getAvatarId()));
245 if (!avatarFile
.exists() && attachmentFile
.exists()) {
247 IOUtils
.createPrivateDirectories(avatarsPath
);
248 Files
.copy(attachmentFile
.toPath(), avatarFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
249 } catch (Exception e
) {
254 JsonGroupStore
.groupsWithLegacyAvatarId
.clear();
257 if (account
.getProfileKey() == null) {
258 // Old config file, creating new profile key
259 account
.setProfileKey(KeyUtils
.createProfileKey());
264 private void createNewIdentity() throws IOException
{
265 IdentityKeyPair identityKey
= KeyHelper
.generateIdentityKeyPair();
266 int registrationId
= KeyHelper
.generateRegistrationId(false);
267 if (username
== null) {
268 account
= SignalAccount
.createTemporaryAccount(identityKey
, registrationId
);
269 account
.setResolver(this::resolveSignalServiceAddress
);
271 ProfileKey profileKey
= KeyUtils
.createProfileKey();
272 account
= SignalAccount
.create(dataPath
, username
, identityKey
, registrationId
, profileKey
);
273 account
.setResolver(this::resolveSignalServiceAddress
);
278 public boolean isRegistered() {
279 return account
!= null && account
.isRegistered();
282 public void register(boolean voiceVerification
) throws IOException
{
283 if (account
== null) {
286 account
.setPassword(KeyUtils
.createPassword());
287 account
.setUuid(null);
288 accountManager
= getSignalServiceAccountManager();
290 if (voiceVerification
) {
291 accountManager
.requestVoiceVerificationCode(Locale
.getDefault(), Optional
.absent(), Optional
.absent());
293 accountManager
.requestSmsVerificationCode(false, Optional
.absent(), Optional
.absent());
296 account
.setRegistered(false);
300 public void updateAccountAttributes() throws IOException
{
301 accountManager
.setAccountAttributes(account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, account
.getRegistrationLockPin(), account
.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, BaseConfig
.capabilities
);
304 public void setProfileName(String name
) throws IOException
{
305 accountManager
.setProfileName(account
.getProfileKey(), name
);
308 public void setProfileAvatar(File avatar
) throws IOException
{
309 final StreamDetails streamDetails
= Utils
.createStreamDetailsFromFile(avatar
);
310 accountManager
.setProfileAvatar(account
.getProfileKey(), streamDetails
);
311 streamDetails
.getStream().close();
314 public void removeProfileAvatar() throws IOException
{
315 accountManager
.setProfileAvatar(account
.getProfileKey(), null);
318 public void unregister() throws IOException
{
319 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
320 // If this is the master device, other users can't send messages to this number anymore.
321 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
322 accountManager
.setGcmId(Optional
.absent());
324 account
.setRegistered(false);
328 public String
getDeviceLinkUri() throws TimeoutException
, IOException
{
329 if (account
== null) {
332 account
.setPassword(KeyUtils
.createPassword());
333 accountManager
= getSignalServiceAccountManager();
334 String uuid
= accountManager
.getNewDeviceUuid();
336 return Utils
.createDeviceLinkUri(new Utils
.DeviceLinkInfo(uuid
, getIdentity().getPublicKey()));
339 public void finishDeviceLink(String deviceName
) throws IOException
, InvalidKeyException
, TimeoutException
, UserAlreadyExists
{
340 account
.setSignalingKey(KeyUtils
.createSignalingKey());
341 SignalServiceAccountManager
.NewDeviceRegistrationReturn ret
= accountManager
.finishNewDeviceRegistration(account
.getSignalProtocolStore().getIdentityKeyPair(), account
.getSignalingKey(), false, true, account
.getSignalProtocolStore().getLocalRegistrationId(), deviceName
);
343 username
= ret
.getNumber();
344 // TODO do this check before actually registering
345 if (SignalAccount
.userExists(dataPath
, username
)) {
346 throw new UserAlreadyExists(username
, SignalAccount
.getFileName(dataPath
, username
));
349 // Create new account with the synced identity
350 byte[] profileKeyBytes
= ret
.getProfileKey();
351 ProfileKey profileKey
;
352 if (profileKeyBytes
== null) {
353 profileKey
= KeyUtils
.createProfileKey();
356 profileKey
= new ProfileKey(profileKeyBytes
);
357 } catch (InvalidInputException e
) {
358 throw new IOException("Received invalid profileKey", e
);
361 account
= SignalAccount
.createLinkedAccount(dataPath
, username
, ret
.getUuid(), account
.getPassword(), ret
.getDeviceId(), ret
.getIdentity(), account
.getSignalProtocolStore().getLocalRegistrationId(), account
.getSignalingKey(), profileKey
);
362 account
.setResolver(this::resolveSignalServiceAddress
);
367 requestSyncContacts();
368 requestSyncBlocked();
369 requestSyncConfiguration();
374 public List
<DeviceInfo
> getLinkedDevices() throws IOException
{
375 List
<DeviceInfo
> devices
= accountManager
.getDevices();
376 account
.setMultiDevice(devices
.size() > 1);
381 public void removeLinkedDevices(int deviceId
) throws IOException
{
382 accountManager
.removeDevice(deviceId
);
383 List
<DeviceInfo
> devices
= accountManager
.getDevices();
384 account
.setMultiDevice(devices
.size() > 1);
388 public void addDeviceLink(URI linkUri
) throws IOException
, InvalidKeyException
{
389 Utils
.DeviceLinkInfo info
= Utils
.parseDeviceLinkUri(linkUri
);
391 addDevice(info
.deviceIdentifier
, info
.deviceKey
);
394 private void addDevice(String deviceIdentifier
, ECPublicKey deviceKey
) throws IOException
, InvalidKeyException
{
395 IdentityKeyPair identityKeyPair
= account
.getSignalProtocolStore().getIdentityKeyPair();
396 String verificationCode
= accountManager
.getNewDeviceVerificationCode();
398 accountManager
.addDevice(deviceIdentifier
, deviceKey
, identityKeyPair
, Optional
.of(account
.getProfileKey().serialize()), verificationCode
);
399 account
.setMultiDevice(true);
403 private List
<PreKeyRecord
> generatePreKeys() {
404 List
<PreKeyRecord
> records
= new ArrayList
<>(BaseConfig
.PREKEY_BATCH_SIZE
);
406 final int offset
= account
.getPreKeyIdOffset();
407 for (int i
= 0; i
< BaseConfig
.PREKEY_BATCH_SIZE
; i
++) {
408 int preKeyId
= (offset
+ i
) % Medium
.MAX_VALUE
;
409 ECKeyPair keyPair
= Curve
.generateKeyPair();
410 PreKeyRecord
record = new PreKeyRecord(preKeyId
, keyPair
);
415 account
.addPreKeys(records
);
421 private SignedPreKeyRecord
generateSignedPreKey(IdentityKeyPair identityKeyPair
) {
423 ECKeyPair keyPair
= Curve
.generateKeyPair();
424 byte[] signature
= Curve
.calculateSignature(identityKeyPair
.getPrivateKey(), keyPair
.getPublicKey().serialize());
425 SignedPreKeyRecord
record = new SignedPreKeyRecord(account
.getNextSignedPreKeyId(), System
.currentTimeMillis(), keyPair
, signature
);
427 account
.addSignedPreKey(record);
431 } catch (InvalidKeyException e
) {
432 throw new AssertionError(e
);
436 public void verifyAccount(String verificationCode
, String pin
) throws IOException
{
437 verificationCode
= verificationCode
.replace("-", "");
438 account
.setSignalingKey(KeyUtils
.createSignalingKey());
439 // TODO make unrestricted unidentified access configurable
440 VerifyAccountResponse response
= accountManager
.verifyAccountWithCode(verificationCode
, account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, pin
, null, getSelfUnidentifiedAccessKey(), false, BaseConfig
.capabilities
);
442 UUID uuid
= UuidUtil
.parseOrNull(response
.getUuid());
443 // TODO response.isStorageCapable()
444 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
445 account
.setRegistered(true);
446 account
.setUuid(uuid
);
447 account
.setRegistrationLockPin(pin
);
448 account
.getSignalProtocolStore().saveIdentity(account
.getSelfAddress(), account
.getSignalProtocolStore().getIdentityKeyPair().getPublicKey(), TrustLevel
.TRUSTED_VERIFIED
);
454 public void setRegistrationLockPin(Optional
<String
> pin
) throws IOException
{
455 if (pin
.isPresent()) {
456 account
.setRegistrationLockPin(pin
.get());
457 throw new RuntimeException("Not implemented anymore, will be replaced with KBS");
459 account
.setRegistrationLockPin(null);
460 accountManager
.removeRegistrationLockV1();
465 private void refreshPreKeys() throws IOException
{
466 List
<PreKeyRecord
> oneTimePreKeys
= generatePreKeys();
467 final IdentityKeyPair identityKeyPair
= account
.getSignalProtocolStore().getIdentityKeyPair();
468 SignedPreKeyRecord signedPreKeyRecord
= generateSignedPreKey(identityKeyPair
);
470 accountManager
.setPreKeys(getIdentity(), signedPreKeyRecord
, oneTimePreKeys
);
473 private SignalServiceMessageReceiver
getMessageReceiver() {
474 // TODO implement ZkGroup support
475 final ClientZkProfileOperations clientZkProfileOperations
= null;
476 return new SignalServiceMessageReceiver(BaseConfig
.serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), account
.getSignalingKey(), BaseConfig
.USER_AGENT
, null, timer
, clientZkProfileOperations
);
479 private SignalServiceMessageSender
getMessageSender() {
480 // TODO implement ZkGroup support
481 final ClientZkProfileOperations clientZkProfileOperations
= null;
482 final boolean attachmentsV3
= false;
483 return new SignalServiceMessageSender(BaseConfig
.serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(),
484 account
.getDeviceId(), account
.getSignalProtocolStore(), BaseConfig
.USER_AGENT
, account
.isMultiDevice(), attachmentsV3
, Optional
.fromNullable(messagePipe
), Optional
.fromNullable(unidentifiedMessagePipe
), Optional
.absent(), clientZkProfileOperations
);
487 private SignalServiceProfile
getRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
) throws IOException
{
488 SignalServiceMessagePipe pipe
= unidentifiedMessagePipe
!= null && unidentifiedAccess
.isPresent() ? unidentifiedMessagePipe
493 return pipe
.getProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
494 } catch (IOException ignored
) {
498 SignalServiceMessageReceiver receiver
= getMessageReceiver();
500 return receiver
.retrieveProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
501 } catch (VerificationFailedException e
) {
502 throw new AssertionError(e
);
506 private Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(byte[] groupId
) throws IOException
{
507 File file
= getGroupAvatarFile(groupId
);
508 if (!file
.exists()) {
509 return Optional
.absent();
512 return Optional
.of(Utils
.createAttachment(file
));
515 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(String number
) throws IOException
{
516 File file
= getContactAvatarFile(number
);
517 if (!file
.exists()) {
518 return Optional
.absent();
521 return Optional
.of(Utils
.createAttachment(file
));
524 private GroupInfo
getGroupForSending(byte[] groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
525 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
527 throw new GroupNotFoundException(groupId
);
529 if (!g
.isMember(account
.getSelfAddress())) {
530 throw new NotAGroupMemberException(groupId
, g
.name
);
535 public List
<GroupInfo
> getGroups() {
536 return account
.getGroupStore().getGroups();
540 public long sendGroupMessage(String messageText
, List
<String
> attachments
,
542 throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
543 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
544 if (attachments
!= null) {
545 messageBuilder
.withAttachments(Utils
.getSignalServiceAttachments(attachments
));
547 if (groupId
!= null) {
548 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
551 messageBuilder
.asGroupMessage(group
);
554 final GroupInfo g
= getGroupForSending(groupId
);
556 messageBuilder
.withExpiration(g
.messageExpirationTime
);
558 return sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
561 public void sendGroupMessageReaction(String emoji
, boolean remove
, String targetAuthor
,
562 long targetSentTimestamp
, byte[] groupId
)
563 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
564 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, canonicalizeAndResolveSignalServiceAddress(targetAuthor
), targetSentTimestamp
);
565 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
566 .withReaction(reaction
);
567 if (groupId
!= null) {
568 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
571 messageBuilder
.asGroupMessage(group
);
573 final GroupInfo g
= getGroupForSending(groupId
);
574 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
577 public void sendQuitGroupMessage(byte[] groupId
) throws GroupNotFoundException
, IOException
, EncapsulatedExceptions
{
578 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
582 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
583 .asGroupMessage(group
);
585 final GroupInfo g
= getGroupForSending(groupId
);
586 g
.removeMember(account
.getSelfAddress());
587 account
.getGroupStore().updateGroup(g
);
589 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
592 private byte[] sendUpdateGroupMessage(byte[] groupId
, String name
, Collection
<SignalServiceAddress
> members
, String avatarFile
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
594 if (groupId
== null) {
596 g
= new GroupInfo(KeyUtils
.createGroupId());
597 g
.addMembers(Collections
.singleton(account
.getSelfAddress()));
599 g
= getGroupForSending(groupId
);
606 if (members
!= null) {
607 final Set
<String
> newE164Members
= new HashSet
<>();
608 for (SignalServiceAddress member
: members
) {
609 if (g
.isMember(member
) || !member
.getNumber().isPresent()) {
612 newE164Members
.add(member
.getNumber().get());
615 final List
<ContactTokenDetails
> contacts
= accountManager
.getContacts(newE164Members
);
616 if (contacts
.size() != newE164Members
.size()) {
617 // Some of the new members are not registered on Signal
618 for (ContactTokenDetails contact
: contacts
) {
619 newE164Members
.remove(contact
.getNumber());
621 System
.err
.println("Failed to add members " + Util
.join(", ", newE164Members
) + " to group: Not registered on Signal");
622 System
.err
.println("Aborting…");
626 g
.addMembers(members
);
629 if (avatarFile
!= null) {
630 IOUtils
.createPrivateDirectories(avatarsPath
);
631 File aFile
= getGroupAvatarFile(g
.groupId
);
632 Files
.copy(Paths
.get(avatarFile
), aFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
635 account
.getGroupStore().updateGroup(g
);
637 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
639 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
643 private void sendUpdateGroupMessage(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
644 if (groupId
== null) {
647 GroupInfo g
= getGroupForSending(groupId
);
649 if (!g
.isMember(recipient
)) {
653 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
655 // Send group message only to the recipient who requested it
656 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
659 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfo g
) {
660 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
663 .withMembers(new ArrayList
<>(g
.getMembers()));
665 File aFile
= getGroupAvatarFile(g
.groupId
);
666 if (aFile
.exists()) {
668 group
.withAvatar(Utils
.createAttachment(aFile
));
669 } catch (IOException e
) {
670 throw new AttachmentInvalidException(aFile
.toString(), e
);
674 return SignalServiceDataMessage
.newBuilder()
675 .asGroupMessage(group
.build())
676 .withExpiration(g
.messageExpirationTime
);
679 private void sendGroupInfoRequest(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
680 if (groupId
== null) {
684 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
)
687 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
688 .asGroupMessage(group
.build());
690 // Send group info request message to the recipient who sent us a message with this groupId
691 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
694 private void sendReceipt(SignalServiceAddress remoteAddress
, long messageId
) throws IOException
, UntrustedIdentityException
{
695 SignalServiceReceiptMessage receiptMessage
= new SignalServiceReceiptMessage(SignalServiceReceiptMessage
.Type
.DELIVERY
,
696 Collections
.singletonList(messageId
),
697 System
.currentTimeMillis());
699 getMessageSender().sendReceipt(remoteAddress
, getAccessFor(remoteAddress
), receiptMessage
);
703 public long sendMessage(String message
, List
<String
> attachments
, String recipient
)
704 throws EncapsulatedExceptions
, AttachmentInvalidException
, IOException
, InvalidNumberException
{
705 List
<String
> recipients
= new ArrayList
<>(1);
706 recipients
.add(recipient
);
707 return sendMessage(message
, attachments
, recipients
);
711 public long sendMessage(String messageText
, List
<String
> attachments
,
712 List
<String
> recipients
)
713 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
714 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
715 if (attachments
!= null) {
716 List
<SignalServiceAttachment
> attachmentStreams
= Utils
.getSignalServiceAttachments(attachments
);
718 // Upload attachments here, so we only upload once even for multiple recipients
719 SignalServiceMessageSender messageSender
= getMessageSender();
720 List
<SignalServiceAttachment
> attachmentPointers
= new ArrayList
<>(attachmentStreams
.size());
721 for (SignalServiceAttachment attachment
: attachmentStreams
) {
722 if (attachment
.isStream()) {
723 attachmentPointers
.add(messageSender
.uploadAttachment(attachment
.asStream()));
724 } else if (attachment
.isPointer()) {
725 attachmentPointers
.add(attachment
.asPointer());
729 messageBuilder
.withAttachments(attachmentPointers
);
731 return sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
734 public void sendMessageReaction(String emoji
, boolean remove
, String targetAuthor
,
735 long targetSentTimestamp
, List
<String
> recipients
)
736 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
737 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, canonicalizeAndResolveSignalServiceAddress(targetAuthor
), targetSentTimestamp
);
738 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
739 .withReaction(reaction
);
740 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
744 public void sendEndSessionMessage(List
<String
> recipients
) throws IOException
, EncapsulatedExceptions
, InvalidNumberException
{
745 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
746 .asEndSessionMessage();
748 final Collection
<SignalServiceAddress
> signalServiceAddresses
= getSignalServiceAddresses(recipients
);
750 sendMessageLegacy(messageBuilder
, signalServiceAddresses
);
751 } catch (Exception e
) {
752 for (SignalServiceAddress address
: signalServiceAddresses
) {
753 handleEndSession(address
);
760 public String
getContactName(String number
) throws InvalidNumberException
{
761 ContactInfo contact
= account
.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number
));
762 if (contact
== null) {
770 public void setContactName(String number
, String name
) throws InvalidNumberException
{
771 final SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(number
);
772 ContactInfo contact
= account
.getContactStore().getContact(address
);
773 if (contact
== null) {
774 contact
= new ContactInfo(address
);
775 System
.err
.println("Add contact " + contact
.number
+ " named " + name
);
777 System
.err
.println("Updating contact " + contact
.number
+ " name " + contact
.name
+ " -> " + name
);
780 account
.getContactStore().updateContact(contact
);
785 public void setContactBlocked(String number
, boolean blocked
) throws InvalidNumberException
{
786 setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number
), blocked
);
789 private void setContactBlocked(SignalServiceAddress address
, boolean blocked
) {
790 ContactInfo contact
= account
.getContactStore().getContact(address
);
791 if (contact
== null) {
792 contact
= new ContactInfo(address
);
793 System
.err
.println("Adding and " + (blocked ?
"blocking" : "unblocking") + " contact " + address
.getNumber().orNull());
795 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " contact " + address
.getNumber().orNull());
797 contact
.blocked
= blocked
;
798 account
.getContactStore().updateContact(contact
);
803 public void setGroupBlocked(final byte[] groupId
, final boolean blocked
) throws GroupNotFoundException
{
804 GroupInfo group
= getGroup(groupId
);
806 throw new GroupNotFoundException(groupId
);
808 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " group " + Base64
.encodeBytes(groupId
));
809 group
.blocked
= blocked
;
810 account
.getGroupStore().updateGroup(group
);
816 public List
<byte[]> getGroupIds() {
817 List
<GroupInfo
> groups
= getGroups();
818 List
<byte[]> ids
= new ArrayList
<>(groups
.size());
819 for (GroupInfo group
: groups
) {
820 ids
.add(group
.groupId
);
826 public String
getGroupName(byte[] groupId
) {
827 GroupInfo group
= getGroup(groupId
);
836 public List
<String
> getGroupMembers(byte[] groupId
) {
837 GroupInfo group
= getGroup(groupId
);
839 return Collections
.emptyList();
841 return new ArrayList
<>(group
.getMembersE164());
846 public byte[] updateGroup(byte[] groupId
, String name
, List
<String
> members
, String avatar
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
, InvalidNumberException
{
847 if (groupId
.length
== 0) {
850 if (name
.isEmpty()) {
853 if (members
.size() == 0) {
856 if (avatar
.isEmpty()) {
859 return sendUpdateGroupMessage(groupId
, name
, members
== null ?
null : getSignalServiceAddresses(members
), avatar
);
863 * Change the expiration timer for a contact
865 public void setExpirationTimer(SignalServiceAddress address
, int messageExpirationTimer
) {
866 ContactInfo c
= account
.getContactStore().getContact(address
);
867 c
.messageExpirationTime
= messageExpirationTimer
;
868 account
.getContactStore().updateContact(c
);
872 * Change the expiration timer for a group
874 public void setExpirationTimer(byte[] groupId
, int messageExpirationTimer
) {
875 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
876 g
.messageExpirationTime
= messageExpirationTimer
;
877 account
.getGroupStore().updateGroup(g
);
881 * Upload the sticker pack from path.
883 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
884 * @return if successful, returns the URL to install the sticker pack in the signal app
886 public String
uploadStickerPack(String path
) throws IOException
, StickerPackInvalidException
{
887 SignalServiceStickerManifestUpload manifest
= getSignalServiceStickerManifestUpload(path
);
889 SignalServiceMessageSender messageSender
= getMessageSender();
891 byte[] packKey
= KeyUtils
.createStickerUploadKey();
892 String packId
= messageSender
.uploadStickerManifest(manifest
, packKey
);
895 return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder
.encode(packId
, "utf-8") + "&pack_key=" + URLEncoder
.encode(Hex
.toStringCondensed(packKey
), "utf-8"))
897 } catch (URISyntaxException e
) {
898 throw new AssertionError(e
);
902 private SignalServiceStickerManifestUpload
getSignalServiceStickerManifestUpload(final String path
) throws IOException
, StickerPackInvalidException
{
904 String rootPath
= null;
906 final File file
= new File(path
);
907 if (file
.getName().endsWith(".zip")) {
908 zip
= new ZipFile(file
);
909 } else if (file
.getName().equals("manifest.json")) {
910 rootPath
= file
.getParent();
912 throw new StickerPackInvalidException("Could not find manifest.json");
915 JsonStickerPack pack
= parseStickerPack(rootPath
, zip
);
917 if (pack
.stickers
== null) {
918 throw new StickerPackInvalidException("Must set a 'stickers' field.");
921 if (pack
.stickers
.isEmpty()) {
922 throw new StickerPackInvalidException("Must include stickers.");
925 List
<StickerInfo
> stickers
= new ArrayList
<>(pack
.stickers
.size());
926 for (JsonStickerPack
.JsonSticker sticker
: pack
.stickers
) {
927 if (sticker
.file
== null) {
928 throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
931 Pair
<InputStream
, Long
> data
;
933 data
= getInputStreamAndLength(rootPath
, zip
, sticker
.file
);
934 } catch (IOException ignored
) {
935 throw new StickerPackInvalidException("Could not find find " + sticker
.file
);
938 StickerInfo stickerInfo
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(sticker
.emoji
).or(""));
939 stickers
.add(stickerInfo
);
942 StickerInfo cover
= null;
943 if (pack
.cover
!= null) {
944 if (pack
.cover
.file
== null) {
945 throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
948 Pair
<InputStream
, Long
> data
;
950 data
= getInputStreamAndLength(rootPath
, zip
, pack
.cover
.file
);
951 } catch (IOException ignored
) {
952 throw new StickerPackInvalidException("Could not find find " + pack
.cover
.file
);
955 cover
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(pack
.cover
.emoji
).or(""));
958 return new SignalServiceStickerManifestUpload(
965 private static JsonStickerPack
parseStickerPack(String rootPath
, ZipFile zip
) throws IOException
{
966 InputStream inputStream
;
968 inputStream
= zip
.getInputStream(zip
.getEntry("manifest.json"));
970 inputStream
= new FileInputStream((new File(rootPath
, "manifest.json")));
972 return new ObjectMapper().readValue(inputStream
, JsonStickerPack
.class);
975 private static Pair
<InputStream
, Long
> getInputStreamAndLength(final String rootPath
, final ZipFile zip
, final String subfile
) throws IOException
{
977 final ZipEntry entry
= zip
.getEntry(subfile
);
978 return new Pair
<>(zip
.getInputStream(entry
), entry
.getSize());
980 final File file
= new File(rootPath
, subfile
);
981 return new Pair
<>(new FileInputStream(file
), file
.length());
985 private void requestSyncGroups() throws IOException
{
986 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
).build();
987 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
989 sendSyncMessage(message
);
990 } catch (UntrustedIdentityException e
) {
995 private void requestSyncContacts() throws IOException
{
996 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
).build();
997 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
999 sendSyncMessage(message
);
1000 } catch (UntrustedIdentityException e
) {
1001 e
.printStackTrace();
1005 private void requestSyncBlocked() throws IOException
{
1006 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
).build();
1007 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1009 sendSyncMessage(message
);
1010 } catch (UntrustedIdentityException e
) {
1011 e
.printStackTrace();
1015 private void requestSyncConfiguration() throws IOException
{
1016 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
).build();
1017 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1019 sendSyncMessage(message
);
1020 } catch (UntrustedIdentityException e
) {
1021 e
.printStackTrace();
1025 private byte[] getSenderCertificate() {
1026 // TODO support UUID capable sender certificates
1027 // byte[] certificate = accountManager.getSenderCertificate();
1030 certificate
= accountManager
.getSenderCertificateLegacy();
1031 } catch (IOException e
) {
1032 System
.err
.println("Failed to get sender certificate: " + e
);
1035 // TODO cache for a day
1039 private byte[] getSelfUnidentifiedAccessKey() {
1040 return UnidentifiedAccess
.deriveAccessKeyFrom(account
.getProfileKey());
1043 private static SignalProfile
decryptProfile(SignalServiceProfile encryptedProfile
, ProfileKey profileKey
) throws IOException
{
1044 ProfileCipher profileCipher
= new ProfileCipher(profileKey
);
1046 return new SignalProfile(
1047 encryptedProfile
.getIdentityKey(),
1048 encryptedProfile
.getName() == null ?
null : new String(profileCipher
.decryptName(Base64
.decode(encryptedProfile
.getName()))),
1049 encryptedProfile
.getAvatar(),
1050 encryptedProfile
.getUnidentifiedAccess() == null || !profileCipher
.verifyUnidentifiedAccess(Base64
.decode(encryptedProfile
.getUnidentifiedAccess())) ?
null : encryptedProfile
.getUnidentifiedAccess(),
1051 encryptedProfile
.isUnrestrictedUnidentifiedAccess()
1053 } catch (InvalidCiphertextException e
) {
1058 private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient
) {
1059 ContactInfo contact
= account
.getContactStore().getContact(recipient
);
1060 if (contact
== null || contact
.profileKey
== null) {
1063 ProfileKey theirProfileKey
;
1065 theirProfileKey
= new ProfileKey(Base64
.decode(contact
.profileKey
));
1066 } catch (InvalidInputException
| IOException e
) {
1067 throw new AssertionError(e
);
1069 SignalProfile targetProfile
;
1071 targetProfile
= decryptProfile(getRecipientProfile(recipient
, Optional
.absent()), theirProfileKey
);
1072 } catch (IOException e
) {
1073 System
.err
.println("Failed to get recipient profile: " + e
);
1077 if (targetProfile
== null || targetProfile
.getUnidentifiedAccess() == null) {
1081 if (targetProfile
.isUnrestrictedUnidentifiedAccess()) {
1082 return KeyUtils
.createUnrestrictedUnidentifiedAccess();
1085 return UnidentifiedAccess
.deriveAccessKeyFrom(theirProfileKey
);
1088 private Optional
<UnidentifiedAccessPair
> getAccessForSync() {
1089 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1090 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1092 if (selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1093 return Optional
.absent();
1097 return Optional
.of(new UnidentifiedAccessPair(
1098 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1099 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1101 } catch (InvalidCertificateException e
) {
1102 return Optional
.absent();
1106 private List
<Optional
<UnidentifiedAccessPair
>> getAccessFor(Collection
<SignalServiceAddress
> recipients
) {
1107 List
<Optional
<UnidentifiedAccessPair
>> result
= new ArrayList
<>(recipients
.size());
1108 for (SignalServiceAddress recipient
: recipients
) {
1109 result
.add(getAccessFor(recipient
));
1114 private Optional
<UnidentifiedAccessPair
> getAccessFor(SignalServiceAddress recipient
) {
1115 byte[] recipientUnidentifiedAccessKey
= getTargetUnidentifiedAccessKey(recipient
);
1116 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1117 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1119 if (recipientUnidentifiedAccessKey
== null || selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1120 return Optional
.absent();
1124 return Optional
.of(new UnidentifiedAccessPair(
1125 new UnidentifiedAccess(recipientUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1126 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1128 } catch (InvalidCertificateException e
) {
1129 return Optional
.absent();
1133 private Optional
<UnidentifiedAccess
> getUnidentifiedAccess(SignalServiceAddress recipient
) {
1134 Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1136 if (unidentifiedAccess
.isPresent()) {
1137 return unidentifiedAccess
.get().getTargetUnidentifiedAccess();
1140 return Optional
.absent();
1143 private void sendSyncMessage(SignalServiceSyncMessage message
)
1144 throws IOException
, UntrustedIdentityException
{
1145 SignalServiceMessageSender messageSender
= getMessageSender();
1147 messageSender
.sendMessage(message
, getAccessForSync());
1148 } catch (UntrustedIdentityException e
) {
1149 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1155 * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult.
1157 private long sendMessageLegacy(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1158 throws EncapsulatedExceptions
, IOException
{
1159 final long timestamp
= System
.currentTimeMillis();
1160 messageBuilder
.withTimestamp(timestamp
);
1161 List
<SendMessageResult
> results
= sendMessage(messageBuilder
, recipients
);
1163 List
<UntrustedIdentityException
> untrustedIdentities
= new LinkedList
<>();
1164 List
<UnregisteredUserException
> unregisteredUsers
= new LinkedList
<>();
1165 List
<NetworkFailureException
> networkExceptions
= new LinkedList
<>();
1167 for (SendMessageResult result
: results
) {
1168 if (result
.isUnregisteredFailure()) {
1169 unregisteredUsers
.add(new UnregisteredUserException(result
.getAddress().getLegacyIdentifier(), null));
1170 } else if (result
.isNetworkFailure()) {
1171 networkExceptions
.add(new NetworkFailureException(result
.getAddress().getLegacyIdentifier(), null));
1172 } else if (result
.getIdentityFailure() != null) {
1173 untrustedIdentities
.add(new UntrustedIdentityException("Untrusted", result
.getAddress().getLegacyIdentifier(), result
.getIdentityFailure().getIdentityKey()));
1176 if (!untrustedIdentities
.isEmpty() || !unregisteredUsers
.isEmpty() || !networkExceptions
.isEmpty()) {
1177 throw new EncapsulatedExceptions(untrustedIdentities
, unregisteredUsers
, networkExceptions
);
1182 private Collection
<SignalServiceAddress
> getSignalServiceAddresses(Collection
<String
> numbers
) throws InvalidNumberException
{
1183 final Set
<SignalServiceAddress
> signalServiceAddresses
= new HashSet
<>(numbers
.size());
1185 for (String number
: numbers
) {
1186 signalServiceAddresses
.add(canonicalizeAndResolveSignalServiceAddress(number
));
1188 return signalServiceAddresses
;
1191 private List
<SendMessageResult
> sendMessage(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1192 throws IOException
{
1193 if (messagePipe
== null) {
1194 messagePipe
= getMessageReceiver().createMessagePipe();
1196 if (unidentifiedMessagePipe
== null) {
1197 unidentifiedMessagePipe
= getMessageReceiver().createUnidentifiedMessagePipe();
1199 SignalServiceDataMessage message
= null;
1201 SignalServiceMessageSender messageSender
= getMessageSender();
1203 message
= messageBuilder
.build();
1204 if (message
.getGroupContext().isPresent()) {
1206 final boolean isRecipientUpdate
= false;
1207 List
<SendMessageResult
> result
= messageSender
.sendMessage(new ArrayList
<>(recipients
), getAccessFor(recipients
), isRecipientUpdate
, message
);
1208 for (SendMessageResult r
: result
) {
1209 if (r
.getIdentityFailure() != null) {
1210 account
.getSignalProtocolStore().saveIdentity(r
.getAddress(), r
.getIdentityFailure().getIdentityKey(), TrustLevel
.UNTRUSTED
);
1214 } catch (UntrustedIdentityException e
) {
1215 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1216 return Collections
.emptyList();
1218 } else if (recipients
.size() == 1 && recipients
.contains(account
.getSelfAddress())) {
1219 SignalServiceAddress recipient
= account
.getSelfAddress();
1220 final Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1221 SentTranscriptMessage transcript
= new SentTranscriptMessage(Optional
.of(recipient
),
1222 message
.getTimestamp(),
1224 message
.getExpiresInSeconds(),
1225 Collections
.singletonMap(recipient
, unidentifiedAccess
.isPresent()),
1227 SignalServiceSyncMessage syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
1229 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1231 messageSender
.sendMessage(syncMessage
, unidentifiedAccess
);
1232 } catch (UntrustedIdentityException e
) {
1233 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1234 results
.add(SendMessageResult
.identityFailure(recipient
, e
.getIdentityKey()));
1238 // Send to all individually, so sync messages are sent correctly
1239 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1240 for (SignalServiceAddress address
: recipients
) {
1241 ContactInfo contact
= account
.getContactStore().getContact(address
);
1242 if (contact
!= null) {
1243 messageBuilder
.withExpiration(contact
.messageExpirationTime
);
1244 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
1246 messageBuilder
.withExpiration(0);
1247 messageBuilder
.withProfileKey(null);
1249 message
= messageBuilder
.build();
1251 SendMessageResult result
= messageSender
.sendMessage(address
, getAccessFor(address
), message
);
1252 results
.add(result
);
1253 } catch (UntrustedIdentityException e
) {
1254 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1255 results
.add(SendMessageResult
.identityFailure(address
, e
.getIdentityKey()));
1261 if (message
!= null && message
.isEndSession()) {
1262 for (SignalServiceAddress recipient
: recipients
) {
1263 handleEndSession(recipient
);
1270 private SignalServiceContent
decryptMessage(SignalServiceEnvelope envelope
) throws InvalidMetadataMessageException
, ProtocolInvalidMessageException
, ProtocolDuplicateMessageException
, ProtocolLegacyMessageException
, ProtocolInvalidKeyIdException
, InvalidMetadataVersionException
, ProtocolInvalidVersionException
, ProtocolNoSessionException
, ProtocolInvalidKeyException
, SelfSendException
, UnsupportedDataMessageException
, org
.whispersystems
.libsignal
.UntrustedIdentityException
{
1271 SignalServiceCipher cipher
= new SignalServiceCipher(account
.getSelfAddress(), account
.getSignalProtocolStore(), Utils
.getCertificateValidator());
1273 return cipher
.decrypt(envelope
);
1274 } catch (ProtocolUntrustedIdentityException e
) {
1275 if (e
.getCause() instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
) {
1276 org
.whispersystems
.libsignal
.UntrustedIdentityException identityException
= (org
.whispersystems
.libsignal
.UntrustedIdentityException
) e
.getCause();
1277 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(identityException
.getName()), identityException
.getUntrustedIdentity(), TrustLevel
.UNTRUSTED
);
1278 throw identityException
;
1280 throw new AssertionError(e
);
1284 private void handleEndSession(SignalServiceAddress source
) {
1285 account
.getSignalProtocolStore().deleteAllSessions(source
);
1288 private void handleSignalServiceDataMessage(SignalServiceDataMessage message
, boolean isSync
, SignalServiceAddress source
, SignalServiceAddress destination
, boolean ignoreAttachments
) {
1289 if (message
.getGroupContext().isPresent() && message
.getGroupContext().get().getGroupV1().isPresent()) {
1290 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1291 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1292 switch (groupInfo
.getType()) {
1294 if (group
== null) {
1295 group
= new GroupInfo(groupInfo
.getGroupId());
1298 if (groupInfo
.getAvatar().isPresent()) {
1299 SignalServiceAttachment avatar
= groupInfo
.getAvatar().get();
1300 if (avatar
.isPointer()) {
1302 retrieveGroupAvatarAttachment(avatar
.asPointer(), group
.groupId
);
1303 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1304 System
.err
.println("Failed to retrieve group avatar (" + avatar
.asPointer().getRemoteId() + "): " + e
.getMessage());
1309 if (groupInfo
.getName().isPresent()) {
1310 group
.name
= groupInfo
.getName().get();
1313 if (groupInfo
.getMembers().isPresent()) {
1314 group
.addMembers(groupInfo
.getMembers().get()
1316 .map(this::resolveSignalServiceAddress
)
1317 .collect(Collectors
.toSet()));
1320 account
.getGroupStore().updateGroup(group
);
1323 if (group
== null) {
1325 sendGroupInfoRequest(groupInfo
.getGroupId(), source
);
1326 } catch (IOException
| EncapsulatedExceptions e
) {
1327 e
.printStackTrace();
1332 if (group
!= null) {
1333 group
.removeMember(source
);
1334 account
.getGroupStore().updateGroup(group
);
1338 if (group
!= null) {
1340 sendUpdateGroupMessage(groupInfo
.getGroupId(), source
);
1341 } catch (IOException
| EncapsulatedExceptions e
) {
1342 e
.printStackTrace();
1343 } catch (NotAGroupMemberException e
) {
1344 // We have left this group, so don't send a group update message
1350 final SignalServiceAddress conversationPartnerAddress
= isSync ? destination
: source
;
1351 if (message
.isEndSession()) {
1352 handleEndSession(conversationPartnerAddress
);
1354 if (message
.isExpirationUpdate() || message
.getBody().isPresent()) {
1355 if (message
.getGroupContext().isPresent() && message
.getGroupContext().get().getGroupV1().isPresent()) {
1356 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1357 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1358 if (group
== null) {
1359 group
= new GroupInfo(groupInfo
.getGroupId());
1361 if (group
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1362 group
.messageExpirationTime
= message
.getExpiresInSeconds();
1363 account
.getGroupStore().updateGroup(group
);
1366 ContactInfo contact
= account
.getContactStore().getContact(conversationPartnerAddress
);
1367 if (contact
== null) {
1368 contact
= new ContactInfo(conversationPartnerAddress
);
1370 if (contact
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1371 contact
.messageExpirationTime
= message
.getExpiresInSeconds();
1372 account
.getContactStore().updateContact(contact
);
1376 if (message
.getAttachments().isPresent() && !ignoreAttachments
) {
1377 for (SignalServiceAttachment attachment
: message
.getAttachments().get()) {
1378 if (attachment
.isPointer()) {
1380 retrieveAttachment(attachment
.asPointer());
1381 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1382 System
.err
.println("Failed to retrieve attachment (" + attachment
.asPointer().getRemoteId() + "): " + e
.getMessage());
1387 if (message
.getProfileKey().isPresent() && message
.getProfileKey().get().length
== 32) {
1388 if (source
.matches(account
.getSelfAddress())) {
1390 this.account
.setProfileKey(new ProfileKey(message
.getProfileKey().get()));
1391 } catch (InvalidInputException ignored
) {
1393 ContactInfo contact
= account
.getContactStore().getContact(source
);
1394 if (contact
!= null) {
1395 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1396 account
.getContactStore().updateContact(contact
);
1399 ContactInfo contact
= account
.getContactStore().getContact(source
);
1400 if (contact
== null) {
1401 contact
= new ContactInfo(source
);
1403 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1404 account
.getContactStore().updateContact(contact
);
1407 if (message
.getPreviews().isPresent()) {
1408 final List
<SignalServiceDataMessage
.Preview
> previews
= message
.getPreviews().get();
1409 for (SignalServiceDataMessage
.Preview preview
: previews
) {
1410 if (preview
.getImage().isPresent() && preview
.getImage().get().isPointer()) {
1411 SignalServiceAttachmentPointer attachment
= preview
.getImage().get().asPointer();
1413 retrieveAttachment(attachment
);
1414 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1415 System
.err
.println("Failed to retrieve attachment (" + attachment
.getRemoteId() + "): " + e
.getMessage());
1422 private void retryFailedReceivedMessages(ReceiveMessageHandler handler
, boolean ignoreAttachments
) {
1423 final File cachePath
= new File(getMessageCachePath());
1424 if (!cachePath
.exists()) {
1427 for (final File dir
: Objects
.requireNonNull(cachePath
.listFiles())) {
1428 if (!dir
.isDirectory()) {
1429 retryFailedReceivedMessage(handler
, ignoreAttachments
, dir
);
1433 for (final File fileEntry
: Objects
.requireNonNull(dir
.listFiles())) {
1434 if (!fileEntry
.isFile()) {
1437 retryFailedReceivedMessage(handler
, ignoreAttachments
, fileEntry
);
1439 // Try to delete directory if empty
1444 private void retryFailedReceivedMessage(final ReceiveMessageHandler handler
, final boolean ignoreAttachments
, final File fileEntry
) {
1445 SignalServiceEnvelope envelope
;
1447 envelope
= Utils
.loadEnvelope(fileEntry
);
1448 if (envelope
== null) {
1451 } catch (IOException e
) {
1452 e
.printStackTrace();
1455 SignalServiceContent content
= null;
1456 if (!envelope
.isReceipt()) {
1458 content
= decryptMessage(envelope
);
1459 } catch (Exception e
) {
1462 handleMessage(envelope
, content
, ignoreAttachments
);
1465 handler
.handleMessage(envelope
, content
, null);
1467 Files
.delete(fileEntry
.toPath());
1468 } catch (IOException e
) {
1469 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1473 public void receiveMessages(long timeout
, TimeUnit unit
, boolean returnOnTimeout
, boolean ignoreAttachments
, ReceiveMessageHandler handler
) throws IOException
{
1474 retryFailedReceivedMessages(handler
, ignoreAttachments
);
1475 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1478 if (messagePipe
== null) {
1479 messagePipe
= messageReceiver
.createMessagePipe();
1483 SignalServiceEnvelope envelope
;
1484 SignalServiceContent content
= null;
1485 Exception exception
= null;
1486 final long now
= new Date().getTime();
1488 envelope
= messagePipe
.read(timeout
, unit
, envelope1
-> {
1489 // store message on disk, before acknowledging receipt to the server
1491 String source
= envelope1
.getSourceE164().isPresent() ? envelope1
.getSourceE164().get() : "";
1492 File cacheFile
= getMessageCacheFile(source
, now
, envelope1
.getTimestamp());
1493 Utils
.storeEnvelope(envelope1
, cacheFile
);
1494 } catch (IOException e
) {
1495 System
.err
.println("Failed to store encrypted message in disk cache, ignoring: " + e
.getMessage());
1498 } catch (TimeoutException e
) {
1499 if (returnOnTimeout
)
1502 } catch (InvalidVersionException e
) {
1503 System
.err
.println("Ignoring error: " + e
.getMessage());
1506 if (!envelope
.isReceipt()) {
1508 content
= decryptMessage(envelope
);
1509 } catch (Exception e
) {
1512 handleMessage(envelope
, content
, ignoreAttachments
);
1515 if (!isMessageBlocked(envelope
, content
)) {
1516 handler
.handleMessage(envelope
, content
, exception
);
1518 if (!(exception
instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
)) {
1519 File cacheFile
= null;
1521 cacheFile
= getMessageCacheFile(envelope
.getSourceE164().get(), now
, envelope
.getTimestamp());
1522 Files
.delete(cacheFile
.toPath());
1523 // Try to delete directory if empty
1524 new File(getMessageCachePath()).delete();
1525 } catch (IOException e
) {
1526 System
.err
.println("Failed to delete cached message file “" + cacheFile
+ "”: " + e
.getMessage());
1531 if (messagePipe
!= null) {
1532 messagePipe
.shutdown();
1538 private boolean isMessageBlocked(SignalServiceEnvelope envelope
, SignalServiceContent content
) {
1539 SignalServiceAddress source
;
1540 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1541 source
= envelope
.getSourceAddress();
1542 } else if (content
!= null) {
1543 source
= content
.getSender();
1547 ContactInfo sourceContact
= account
.getContactStore().getContact(source
);
1548 if (sourceContact
!= null && sourceContact
.blocked
) {
1552 if (content
!= null && content
.getDataMessage().isPresent()) {
1553 SignalServiceDataMessage message
= content
.getDataMessage().get();
1554 if (message
.getGroupContext().isPresent() && message
.getGroupContext().get().getGroupV1().isPresent()) {
1555 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1556 GroupInfo group
= getGroup(groupInfo
.getGroupId());
1557 if (groupInfo
.getType() == SignalServiceGroup
.Type
.DELIVER
&& group
!= null && group
.blocked
) {
1565 private void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent content
, boolean ignoreAttachments
) {
1566 if (content
!= null) {
1567 SignalServiceAddress sender
;
1568 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1569 sender
= envelope
.getSourceAddress();
1571 sender
= content
.getSender();
1573 if (content
.getDataMessage().isPresent()) {
1574 SignalServiceDataMessage message
= content
.getDataMessage().get();
1576 if (content
.isNeedsReceipt()) {
1578 sendReceipt(sender
, message
.getTimestamp());
1579 } catch (IOException
| UntrustedIdentityException
| IllegalArgumentException e
) {
1580 e
.printStackTrace();
1584 handleSignalServiceDataMessage(message
, false, sender
, account
.getSelfAddress(), ignoreAttachments
);
1586 if (content
.getSyncMessage().isPresent()) {
1587 account
.setMultiDevice(true);
1588 SignalServiceSyncMessage syncMessage
= content
.getSyncMessage().get();
1589 if (syncMessage
.getSent().isPresent()) {
1590 SentTranscriptMessage message
= syncMessage
.getSent().get();
1591 handleSignalServiceDataMessage(message
.getMessage(), true, sender
, message
.getDestination().orNull(), ignoreAttachments
);
1593 if (syncMessage
.getRequest().isPresent()) {
1594 RequestMessage rm
= syncMessage
.getRequest().get();
1595 if (rm
.isContactsRequest()) {
1598 } catch (UntrustedIdentityException
| IOException
| IllegalArgumentException e
) {
1599 e
.printStackTrace();
1602 if (rm
.isGroupsRequest()) {
1605 } catch (UntrustedIdentityException
| IOException
| IllegalArgumentException e
) {
1606 e
.printStackTrace();
1609 if (rm
.isBlockedListRequest()) {
1612 } catch (UntrustedIdentityException
| IOException
| IllegalArgumentException e
) {
1613 e
.printStackTrace();
1616 // TODO Handle rm.isConfigurationRequest();
1618 if (syncMessage
.getGroups().isPresent()) {
1619 File tmpFile
= null;
1621 tmpFile
= IOUtils
.createTempFile();
1622 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(syncMessage
.getGroups().get().asPointer(), tmpFile
)) {
1623 DeviceGroupsInputStream s
= new DeviceGroupsInputStream(attachmentAsStream
);
1625 while ((g
= s
.read()) != null) {
1626 GroupInfo syncGroup
= account
.getGroupStore().getGroup(g
.getId());
1627 if (syncGroup
== null) {
1628 syncGroup
= new GroupInfo(g
.getId());
1630 if (g
.getName().isPresent()) {
1631 syncGroup
.name
= g
.getName().get();
1633 syncGroup
.addMembers(g
.getMembers()
1635 .map(this::resolveSignalServiceAddress
)
1636 .collect(Collectors
.toSet()));
1637 if (!g
.isActive()) {
1638 syncGroup
.removeMember(account
.getSelfAddress());
1640 // Add ourself to the member set as it's marked as active
1641 syncGroup
.addMembers(Collections
.singleton(account
.getSelfAddress()));
1643 syncGroup
.blocked
= g
.isBlocked();
1644 if (g
.getColor().isPresent()) {
1645 syncGroup
.color
= g
.getColor().get();
1648 if (g
.getAvatar().isPresent()) {
1649 retrieveGroupAvatarAttachment(g
.getAvatar().get(), syncGroup
.groupId
);
1651 syncGroup
.inboxPosition
= g
.getInboxPosition().orNull();
1652 syncGroup
.archived
= g
.isArchived();
1653 account
.getGroupStore().updateGroup(syncGroup
);
1656 } catch (Exception e
) {
1657 e
.printStackTrace();
1659 if (tmpFile
!= null) {
1661 Files
.delete(tmpFile
.toPath());
1662 } catch (IOException e
) {
1663 System
.err
.println("Failed to delete received groups temp file “" + tmpFile
+ "”: " + e
.getMessage());
1668 if (syncMessage
.getBlockedList().isPresent()) {
1669 final BlockedListMessage blockedListMessage
= syncMessage
.getBlockedList().get();
1670 for (SignalServiceAddress address
: blockedListMessage
.getAddresses()) {
1671 setContactBlocked(resolveSignalServiceAddress(address
), true);
1673 for (byte[] groupId
: blockedListMessage
.getGroupIds()) {
1675 setGroupBlocked(groupId
, true);
1676 } catch (GroupNotFoundException e
) {
1677 System
.err
.println("BlockedListMessage contained groupID that was not found in GroupStore: " + Base64
.encodeBytes(groupId
));
1681 if (syncMessage
.getContacts().isPresent()) {
1682 File tmpFile
= null;
1684 tmpFile
= IOUtils
.createTempFile();
1685 final ContactsMessage contactsMessage
= syncMessage
.getContacts().get();
1686 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(contactsMessage
.getContactsStream().asPointer(), tmpFile
)) {
1687 DeviceContactsInputStream s
= new DeviceContactsInputStream(attachmentAsStream
);
1688 if (contactsMessage
.isComplete()) {
1689 account
.getContactStore().clear();
1692 while ((c
= s
.read()) != null) {
1693 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
1694 account
.setProfileKey(c
.getProfileKey().get());
1696 final SignalServiceAddress address
= resolveSignalServiceAddress(c
.getAddress());
1697 ContactInfo contact
= account
.getContactStore().getContact(address
);
1698 if (contact
== null) {
1699 contact
= new ContactInfo(address
);
1701 if (c
.getName().isPresent()) {
1702 contact
.name
= c
.getName().get();
1704 if (c
.getColor().isPresent()) {
1705 contact
.color
= c
.getColor().get();
1707 if (c
.getProfileKey().isPresent()) {
1708 contact
.profileKey
= Base64
.encodeBytes(c
.getProfileKey().get().serialize());
1710 if (c
.getVerified().isPresent()) {
1711 final VerifiedMessage verifiedMessage
= c
.getVerified().get();
1712 account
.getSignalProtocolStore().setIdentityTrustLevel(verifiedMessage
.getDestination(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1714 if (c
.getExpirationTimer().isPresent()) {
1715 contact
.messageExpirationTime
= c
.getExpirationTimer().get();
1717 contact
.blocked
= c
.isBlocked();
1718 contact
.inboxPosition
= c
.getInboxPosition().orNull();
1719 contact
.archived
= c
.isArchived();
1720 account
.getContactStore().updateContact(contact
);
1722 if (c
.getAvatar().isPresent()) {
1723 retrieveContactAvatarAttachment(c
.getAvatar().get(), contact
.number
);
1727 } catch (Exception e
) {
1728 e
.printStackTrace();
1730 if (tmpFile
!= null) {
1732 Files
.delete(tmpFile
.toPath());
1733 } catch (IOException e
) {
1734 System
.err
.println("Failed to delete received contacts temp file “" + tmpFile
+ "”: " + e
.getMessage());
1739 if (syncMessage
.getVerified().isPresent()) {
1740 final VerifiedMessage verifiedMessage
= syncMessage
.getVerified().get();
1741 account
.getSignalProtocolStore().setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage
.getDestination()), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1743 if (syncMessage
.getConfiguration().isPresent()) {
1750 private File
getContactAvatarFile(String number
) {
1751 return new File(avatarsPath
, "contact-" + number
);
1754 private File
retrieveContactAvatarAttachment(SignalServiceAttachment attachment
, String number
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1755 IOUtils
.createPrivateDirectories(avatarsPath
);
1756 if (attachment
.isPointer()) {
1757 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1758 return retrieveAttachment(pointer
, getContactAvatarFile(number
), false);
1760 SignalServiceAttachmentStream stream
= attachment
.asStream();
1761 return Utils
.retrieveAttachment(stream
, getContactAvatarFile(number
));
1765 private File
getGroupAvatarFile(byte[] groupId
) {
1766 return new File(avatarsPath
, "group-" + Base64
.encodeBytes(groupId
).replace("/", "_"));
1769 private File
retrieveGroupAvatarAttachment(SignalServiceAttachment attachment
, byte[] groupId
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1770 IOUtils
.createPrivateDirectories(avatarsPath
);
1771 if (attachment
.isPointer()) {
1772 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1773 return retrieveAttachment(pointer
, getGroupAvatarFile(groupId
), false);
1775 SignalServiceAttachmentStream stream
= attachment
.asStream();
1776 return Utils
.retrieveAttachment(stream
, getGroupAvatarFile(groupId
));
1780 public File
getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId
) {
1781 return new File(attachmentsPath
, attachmentId
.toString());
1784 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1785 IOUtils
.createPrivateDirectories(attachmentsPath
);
1786 return retrieveAttachment(pointer
, getAttachmentFile(pointer
.getRemoteId()), true);
1789 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
, File outputFile
, boolean storePreview
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1790 if (storePreview
&& pointer
.getPreview().isPresent()) {
1791 File previewFile
= new File(outputFile
+ ".preview");
1792 try (OutputStream output
= new FileOutputStream(previewFile
)) {
1793 byte[] preview
= pointer
.getPreview().get();
1794 output
.write(preview
, 0, preview
.length
);
1795 } catch (FileNotFoundException e
) {
1796 e
.printStackTrace();
1801 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1803 File tmpFile
= IOUtils
.createTempFile();
1804 try (InputStream input
= messageReceiver
.retrieveAttachment(pointer
, tmpFile
, BaseConfig
.MAX_ATTACHMENT_SIZE
)) {
1805 try (OutputStream output
= new FileOutputStream(outputFile
)) {
1806 byte[] buffer
= new byte[4096];
1809 while ((read
= input
.read(buffer
)) != -1) {
1810 output
.write(buffer
, 0, read
);
1812 } catch (FileNotFoundException e
) {
1813 e
.printStackTrace();
1818 Files
.delete(tmpFile
.toPath());
1819 } catch (IOException e
) {
1820 System
.err
.println("Failed to delete received attachment temp file “" + tmpFile
+ "”: " + e
.getMessage());
1826 private InputStream
retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer
, File tmpFile
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1827 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1828 return messageReceiver
.retrieveAttachment(pointer
, tmpFile
, BaseConfig
.MAX_ATTACHMENT_SIZE
);
1832 public boolean isRemote() {
1837 public String
getObjectPath() {
1841 private void sendGroups() throws IOException
, UntrustedIdentityException
{
1842 File groupsFile
= IOUtils
.createTempFile();
1845 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
1846 DeviceGroupsOutputStream out
= new DeviceGroupsOutputStream(fos
);
1847 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1848 out
.write(new DeviceGroup(record.groupId
, Optional
.fromNullable(record.name
),
1849 new ArrayList
<>(record.getMembers()), createGroupAvatarAttachment(record.groupId
),
1850 record.isMember(account
.getSelfAddress()), Optional
.of(record.messageExpirationTime
),
1851 Optional
.fromNullable(record.color
), record.blocked
, Optional
.fromNullable(record.inboxPosition
), record.archived
));
1855 if (groupsFile
.exists() && groupsFile
.length() > 0) {
1856 try (FileInputStream groupsFileStream
= new FileInputStream(groupsFile
)) {
1857 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1858 .withStream(groupsFileStream
)
1859 .withContentType("application/octet-stream")
1860 .withLength(groupsFile
.length())
1863 sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
1868 Files
.delete(groupsFile
.toPath());
1869 } catch (IOException e
) {
1870 System
.err
.println("Failed to delete groups temp file “" + groupsFile
+ "”: " + e
.getMessage());
1875 public void sendContacts() throws IOException
, UntrustedIdentityException
{
1876 File contactsFile
= IOUtils
.createTempFile();
1879 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
1880 DeviceContactsOutputStream out
= new DeviceContactsOutputStream(fos
);
1881 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1882 VerifiedMessage verifiedMessage
= null;
1883 JsonIdentityKeyStore
.Identity currentIdentity
= account
.getSignalProtocolStore().getIdentity(record.getAddress());
1884 if (currentIdentity
!= null) {
1885 verifiedMessage
= new VerifiedMessage(record.getAddress(), currentIdentity
.getIdentityKey(), currentIdentity
.getTrustLevel().toVerifiedState(), currentIdentity
.getDateAdded().getTime());
1888 ProfileKey profileKey
= null;
1890 profileKey
= record.profileKey
== null ?
null : new ProfileKey(Base64
.decode(record.profileKey
));
1891 } catch (InvalidInputException ignored
) {
1893 out
.write(new DeviceContact(record.getAddress(), Optional
.fromNullable(record.name
),
1894 createContactAvatarAttachment(record.number
), Optional
.fromNullable(record.color
),
1895 Optional
.fromNullable(verifiedMessage
), Optional
.fromNullable(profileKey
), record.blocked
,
1896 Optional
.of(record.messageExpirationTime
),
1897 Optional
.fromNullable(record.inboxPosition
), record.archived
));
1900 if (account
.getProfileKey() != null) {
1901 // Send our own profile key as well
1902 out
.write(new DeviceContact(account
.getSelfAddress(),
1903 Optional
.absent(), Optional
.absent(),
1904 Optional
.absent(), Optional
.absent(),
1905 Optional
.of(account
.getProfileKey()),
1906 false, Optional
.absent(), Optional
.absent(), false));
1910 if (contactsFile
.exists() && contactsFile
.length() > 0) {
1911 try (FileInputStream contactsFileStream
= new FileInputStream(contactsFile
)) {
1912 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1913 .withStream(contactsFileStream
)
1914 .withContentType("application/octet-stream")
1915 .withLength(contactsFile
.length())
1918 sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
, true)));
1923 Files
.delete(contactsFile
.toPath());
1924 } catch (IOException e
) {
1925 System
.err
.println("Failed to delete contacts temp file “" + contactsFile
+ "”: " + e
.getMessage());
1930 private void sendBlockedList() throws IOException
, UntrustedIdentityException
{
1931 List
<SignalServiceAddress
> addresses
= new ArrayList
<>();
1932 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1933 if (record.blocked
) {
1934 addresses
.add(record.getAddress());
1937 List
<byte[]> groupIds
= new ArrayList
<>();
1938 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1939 if (record.blocked
) {
1940 groupIds
.add(record.groupId
);
1943 sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
1946 private void sendVerifiedMessage(SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
) throws IOException
, UntrustedIdentityException
{
1947 VerifiedMessage verifiedMessage
= new VerifiedMessage(destination
, identityKey
, trustLevel
.toVerifiedState(), System
.currentTimeMillis());
1948 sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
1951 public List
<ContactInfo
> getContacts() {
1952 return account
.getContactStore().getContacts();
1955 public ContactInfo
getContact(String number
) {
1956 return account
.getContactStore().getContact(Util
.getSignalServiceAddressFromIdentifier(number
));
1959 public GroupInfo
getGroup(byte[] groupId
) {
1960 return account
.getGroupStore().getGroup(groupId
);
1963 public List
<JsonIdentityKeyStore
.Identity
> getIdentities() {
1964 return account
.getSignalProtocolStore().getIdentities();
1967 public List
<JsonIdentityKeyStore
.Identity
> getIdentities(String number
) throws InvalidNumberException
{
1968 return account
.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number
));
1972 * Trust this the identity with this fingerprint
1974 * @param name username of the identity
1975 * @param fingerprint Fingerprint
1977 public boolean trustIdentityVerified(String name
, byte[] fingerprint
) throws InvalidNumberException
{
1978 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
1979 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
1983 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1984 if (!Arrays
.equals(id
.getIdentityKey().serialize(), fingerprint
)) {
1988 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1990 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1991 } catch (IOException
| UntrustedIdentityException e
) {
1992 e
.printStackTrace();
2001 * Trust this the identity with this safety number
2003 * @param name username of the identity
2004 * @param safetyNumber Safety number
2006 public boolean trustIdentityVerifiedSafetyNumber(String name
, String safetyNumber
) throws InvalidNumberException
{
2007 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
2008 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2012 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2013 if (!safetyNumber
.equals(computeSafetyNumber(address
, id
.getIdentityKey()))) {
2017 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2019 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2020 } catch (IOException
| UntrustedIdentityException e
) {
2021 e
.printStackTrace();
2030 * Trust all keys of this identity without verification
2032 * @param name username of the identity
2034 public boolean trustIdentityAllKeys(String name
) {
2035 SignalServiceAddress address
= resolveSignalServiceAddress(name
);
2036 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2040 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2041 if (id
.getTrustLevel() == TrustLevel
.UNTRUSTED
) {
2042 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2044 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2045 } catch (IOException
| UntrustedIdentityException e
) {
2046 e
.printStackTrace();
2054 public String
computeSafetyNumber(SignalServiceAddress theirAddress
, IdentityKey theirIdentityKey
) {
2055 return Utils
.computeSafetyNumber(account
.getSelfAddress(), getIdentity(), theirAddress
, theirIdentityKey
);
2058 public SignalServiceAddress
canonicalizeAndResolveSignalServiceAddress(String identifier
) throws InvalidNumberException
{
2059 String canonicalizedNumber
= UuidUtil
.isUuid(identifier
) ? identifier
: Util
.canonicalizeNumber(identifier
, account
.getUsername());
2060 return resolveSignalServiceAddress(canonicalizedNumber
);
2063 public SignalServiceAddress
resolveSignalServiceAddress(String identifier
) {
2064 SignalServiceAddress address
= Util
.getSignalServiceAddressFromIdentifier(identifier
);
2066 return resolveSignalServiceAddress(address
);
2069 public SignalServiceAddress
resolveSignalServiceAddress(SignalServiceAddress address
) {
2070 if (address
.matches(account
.getSelfAddress())) {
2071 return account
.getSelfAddress();
2074 return account
.getRecipientStore().resolveServiceAddress(address
);
2077 public interface ReceiveMessageHandler
{
2079 void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent decryptedContent
, Throwable e
);