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
.JsonStickerPack
;
25 import org
.asamk
.signal
.NotAGroupMemberException
;
26 import org
.asamk
.signal
.StickerPackInvalidException
;
27 import org
.asamk
.signal
.TrustLevel
;
28 import org
.asamk
.signal
.UserAlreadyExists
;
29 import org
.asamk
.signal
.storage
.SignalAccount
;
30 import org
.asamk
.signal
.storage
.contacts
.ContactInfo
;
31 import org
.asamk
.signal
.storage
.groups
.GroupInfo
;
32 import org
.asamk
.signal
.storage
.groups
.JsonGroupStore
;
33 import org
.asamk
.signal
.storage
.protocol
.JsonIdentityKeyStore
;
34 import org
.asamk
.signal
.util
.IOUtils
;
35 import org
.asamk
.signal
.util
.Util
;
36 import org
.signal
.libsignal
.metadata
.InvalidMetadataMessageException
;
37 import org
.signal
.libsignal
.metadata
.InvalidMetadataVersionException
;
38 import org
.signal
.libsignal
.metadata
.ProtocolDuplicateMessageException
;
39 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyException
;
40 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyIdException
;
41 import org
.signal
.libsignal
.metadata
.ProtocolInvalidMessageException
;
42 import org
.signal
.libsignal
.metadata
.ProtocolInvalidVersionException
;
43 import org
.signal
.libsignal
.metadata
.ProtocolLegacyMessageException
;
44 import org
.signal
.libsignal
.metadata
.ProtocolNoSessionException
;
45 import org
.signal
.libsignal
.metadata
.ProtocolUntrustedIdentityException
;
46 import org
.signal
.libsignal
.metadata
.SelfSendException
;
47 import org
.signal
.libsignal
.metadata
.certificate
.InvalidCertificateException
;
48 import org
.signal
.zkgroup
.InvalidInputException
;
49 import org
.signal
.zkgroup
.VerificationFailedException
;
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
.SignalServiceAttachmentStream
;
79 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceContent
;
80 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceDataMessage
;
81 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceEnvelope
;
82 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroup
;
83 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceReceiptMessage
;
84 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceStickerManifestUpload
;
85 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceStickerManifestUpload
.StickerInfo
;
86 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.BlockedListMessage
;
87 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ContactsMessage
;
88 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContact
;
89 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsInputStream
;
90 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsOutputStream
;
91 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroup
;
92 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsInputStream
;
93 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsOutputStream
;
94 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceInfo
;
95 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.RequestMessage
;
96 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SentTranscriptMessage
;
97 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SignalServiceSyncMessage
;
98 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.VerifiedMessage
;
99 import org
.whispersystems
.signalservice
.api
.profiles
.SignalServiceProfile
;
100 import org
.whispersystems
.signalservice
.api
.push
.ContactTokenDetails
;
101 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
102 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.AuthorizationFailedException
;
103 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.EncapsulatedExceptions
;
104 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NetworkFailureException
;
105 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.UnregisteredUserException
;
106 import org
.whispersystems
.signalservice
.api
.util
.InvalidNumberException
;
107 import org
.whispersystems
.signalservice
.api
.util
.SleepTimer
;
108 import org
.whispersystems
.signalservice
.api
.util
.StreamDetails
;
109 import org
.whispersystems
.signalservice
.api
.util
.UptimeSleepTimer
;
110 import org
.whispersystems
.signalservice
.internal
.push
.SignalServiceProtos
;
111 import org
.whispersystems
.signalservice
.internal
.push
.UnsupportedDataMessageException
;
112 import org
.whispersystems
.signalservice
.internal
.util
.Hex
;
113 import org
.whispersystems
.util
.Base64
;
116 import java
.io
.FileInputStream
;
117 import java
.io
.FileNotFoundException
;
118 import java
.io
.FileOutputStream
;
119 import java
.io
.IOException
;
120 import java
.io
.InputStream
;
121 import java
.io
.OutputStream
;
123 import java
.net
.URISyntaxException
;
124 import java
.net
.URLEncoder
;
125 import java
.nio
.file
.Files
;
126 import java
.nio
.file
.Paths
;
127 import java
.nio
.file
.StandardCopyOption
;
128 import java
.util
.ArrayList
;
129 import java
.util
.Arrays
;
130 import java
.util
.Collection
;
131 import java
.util
.Collections
;
132 import java
.util
.Date
;
133 import java
.util
.HashSet
;
134 import java
.util
.LinkedList
;
135 import java
.util
.List
;
136 import java
.util
.Locale
;
137 import java
.util
.Map
;
138 import java
.util
.Objects
;
139 import java
.util
.Set
;
140 import java
.util
.UUID
;
141 import java
.util
.concurrent
.TimeUnit
;
142 import java
.util
.concurrent
.TimeoutException
;
143 import java
.util
.zip
.ZipEntry
;
144 import java
.util
.zip
.ZipFile
;
146 public class Manager
implements Signal
{
148 private static final SignalServiceProfile
.Capabilities capabilities
= new SignalServiceProfile
.Capabilities(false, false);
150 private final String settingsPath
;
151 private final String dataPath
;
152 private final String attachmentsPath
;
153 private final String avatarsPath
;
154 private final SleepTimer timer
= new UptimeSleepTimer();
156 private SignalAccount account
;
157 private String username
;
158 private SignalServiceAccountManager accountManager
;
159 private SignalServiceMessagePipe messagePipe
= null;
160 private SignalServiceMessagePipe unidentifiedMessagePipe
= null;
162 public Manager(String username
, String settingsPath
) {
163 this.username
= username
;
164 this.settingsPath
= settingsPath
;
165 this.dataPath
= this.settingsPath
+ "/data";
166 this.attachmentsPath
= this.settingsPath
+ "/attachments";
167 this.avatarsPath
= this.settingsPath
+ "/avatars";
171 public String
getUsername() {
175 public SignalServiceAddress
getSelfAddress() {
176 return account
.getSelfAddress();
179 private SignalServiceAccountManager
getSignalServiceAccountManager() {
180 return new SignalServiceAccountManager(BaseConfig
.serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), BaseConfig
.USER_AGENT
, timer
);
183 private IdentityKey
getIdentity() {
184 return account
.getSignalProtocolStore().getIdentityKeyPair().getPublicKey();
187 public int getDeviceId() {
188 return account
.getDeviceId();
191 private String
getMessageCachePath() {
192 return this.dataPath
+ "/" + username
+ ".d/msg-cache";
195 private String
getMessageCachePath(String sender
) {
196 return getMessageCachePath() + "/" + sender
.replace("/", "_");
199 private File
getMessageCacheFile(String sender
, long now
, long timestamp
) throws IOException
{
200 String cachePath
= getMessageCachePath(sender
);
201 IOUtils
.createPrivateDirectories(cachePath
);
202 return new File(cachePath
+ "/" + now
+ "_" + timestamp
);
205 public boolean userHasKeys() {
206 return account
!= null && account
.getSignalProtocolStore() != null;
209 public void init() throws IOException
{
210 if (!SignalAccount
.userExists(dataPath
, username
)) {
213 account
= SignalAccount
.load(dataPath
, username
);
215 migrateLegacyConfigs();
217 accountManager
= getSignalServiceAccountManager();
219 if (account
.isRegistered()) {
220 if (accountManager
.getPreKeysCount() < BaseConfig
.PREKEY_MINIMUM_COUNT
) {
224 if (account
.getUuid() == null) {
225 account
.setUuid(accountManager
.getOwnUuid());
229 } catch (AuthorizationFailedException e
) {
230 System
.err
.println("Authorization failed, was the number registered elsewhere?");
235 private void migrateLegacyConfigs() {
236 // Copy group avatars that were previously stored in the attachments folder
237 // to the new avatar folder
238 if (JsonGroupStore
.groupsWithLegacyAvatarId
.size() > 0) {
239 for (GroupInfo g
: JsonGroupStore
.groupsWithLegacyAvatarId
) {
240 File avatarFile
= getGroupAvatarFile(g
.groupId
);
241 File attachmentFile
= getAttachmentFile(g
.getAvatarId());
242 if (!avatarFile
.exists() && attachmentFile
.exists()) {
244 IOUtils
.createPrivateDirectories(avatarsPath
);
245 Files
.copy(attachmentFile
.toPath(), avatarFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
246 } catch (Exception e
) {
251 JsonGroupStore
.groupsWithLegacyAvatarId
.clear();
254 if (account
.getProfileKey() == null) {
255 // Old config file, creating new profile key
256 account
.setProfileKey(KeyUtils
.createProfileKey());
261 private void createNewIdentity() throws IOException
{
262 IdentityKeyPair identityKey
= KeyHelper
.generateIdentityKeyPair();
263 int registrationId
= KeyHelper
.generateRegistrationId(false);
264 if (username
== null) {
265 account
= SignalAccount
.createTemporaryAccount(identityKey
, registrationId
);
267 ProfileKey profileKey
= KeyUtils
.createProfileKey();
268 account
= SignalAccount
.create(dataPath
, username
, identityKey
, registrationId
, profileKey
);
273 public boolean isRegistered() {
274 return account
!= null && account
.isRegistered();
277 public void register(boolean voiceVerification
) throws IOException
{
278 if (account
== null) {
281 account
.setPassword(KeyUtils
.createPassword());
282 accountManager
= getSignalServiceAccountManager();
284 if (voiceVerification
) {
285 accountManager
.requestVoiceVerificationCode(Locale
.getDefault(), Optional
.absent(), Optional
.absent());
287 accountManager
.requestSmsVerificationCode(false, Optional
.absent(), Optional
.absent());
290 account
.setRegistered(false);
294 public void updateAccountAttributes() throws IOException
{
295 accountManager
.setAccountAttributes(account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, account
.getRegistrationLockPin(), account
.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities
);
298 public void setProfileName(String name
) throws IOException
{
299 accountManager
.setProfileName(account
.getProfileKey(), name
);
302 public void setProfileAvatar(File avatar
) throws IOException
{
303 final StreamDetails streamDetails
= Utils
.createStreamDetailsFromFile(avatar
);
304 accountManager
.setProfileAvatar(account
.getProfileKey(), streamDetails
);
305 streamDetails
.getStream().close();
308 public void removeProfileAvatar() throws IOException
{
309 accountManager
.setProfileAvatar(account
.getProfileKey(), null);
312 public void unregister() throws IOException
{
313 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
314 // If this is the master device, other users can't send messages to this number anymore.
315 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
316 accountManager
.setGcmId(Optional
.absent());
318 account
.setRegistered(false);
322 public String
getDeviceLinkUri() throws TimeoutException
, IOException
{
323 if (account
== null) {
326 account
.setPassword(KeyUtils
.createPassword());
327 accountManager
= getSignalServiceAccountManager();
328 String uuid
= accountManager
.getNewDeviceUuid();
330 return Utils
.createDeviceLinkUri(new Utils
.DeviceLinkInfo(uuid
, getIdentity().getPublicKey()));
333 public void finishDeviceLink(String deviceName
) throws IOException
, InvalidKeyException
, TimeoutException
, UserAlreadyExists
{
334 account
.setSignalingKey(KeyUtils
.createSignalingKey());
335 SignalServiceAccountManager
.NewDeviceRegistrationReturn ret
= accountManager
.finishNewDeviceRegistration(account
.getSignalProtocolStore().getIdentityKeyPair(), account
.getSignalingKey(), false, true, account
.getSignalProtocolStore().getLocalRegistrationId(), deviceName
);
337 username
= ret
.getNumber();
338 // TODO do this check before actually registering
339 if (SignalAccount
.userExists(dataPath
, username
)) {
340 throw new UserAlreadyExists(username
, SignalAccount
.getFileName(dataPath
, username
));
343 // Create new account with the synced identity
344 byte[] profileKeyBytes
= ret
.getProfileKey();
345 ProfileKey profileKey
;
346 if (profileKeyBytes
== null) {
347 profileKey
= KeyUtils
.createProfileKey();
350 profileKey
= new ProfileKey(profileKeyBytes
);
351 } catch (InvalidInputException e
) {
352 throw new IOException("Received invalid profileKey", e
);
355 account
= SignalAccount
.createLinkedAccount(dataPath
, username
, ret
.getUuid(), account
.getPassword(), ret
.getDeviceId(), ret
.getIdentity(), account
.getSignalProtocolStore().getLocalRegistrationId(), account
.getSignalingKey(), profileKey
);
360 requestSyncContacts();
361 requestSyncBlocked();
362 requestSyncConfiguration();
367 public List
<DeviceInfo
> getLinkedDevices() throws IOException
{
368 List
<DeviceInfo
> devices
= accountManager
.getDevices();
369 account
.setMultiDevice(devices
.size() > 1);
374 public void removeLinkedDevices(int deviceId
) throws IOException
{
375 accountManager
.removeDevice(deviceId
);
376 List
<DeviceInfo
> devices
= accountManager
.getDevices();
377 account
.setMultiDevice(devices
.size() > 1);
381 public void addDeviceLink(URI linkUri
) throws IOException
, InvalidKeyException
{
382 Utils
.DeviceLinkInfo info
= Utils
.parseDeviceLinkUri(linkUri
);
384 addDevice(info
.deviceIdentifier
, info
.deviceKey
);
387 private void addDevice(String deviceIdentifier
, ECPublicKey deviceKey
) throws IOException
, InvalidKeyException
{
388 IdentityKeyPair identityKeyPair
= account
.getSignalProtocolStore().getIdentityKeyPair();
389 String verificationCode
= accountManager
.getNewDeviceVerificationCode();
391 accountManager
.addDevice(deviceIdentifier
, deviceKey
, identityKeyPair
, Optional
.of(account
.getProfileKey().serialize()), verificationCode
);
392 account
.setMultiDevice(true);
396 private List
<PreKeyRecord
> generatePreKeys() {
397 List
<PreKeyRecord
> records
= new ArrayList
<>(BaseConfig
.PREKEY_BATCH_SIZE
);
399 final int offset
= account
.getPreKeyIdOffset();
400 for (int i
= 0; i
< BaseConfig
.PREKEY_BATCH_SIZE
; i
++) {
401 int preKeyId
= (offset
+ i
) % Medium
.MAX_VALUE
;
402 ECKeyPair keyPair
= Curve
.generateKeyPair();
403 PreKeyRecord
record = new PreKeyRecord(preKeyId
, keyPair
);
408 account
.addPreKeys(records
);
414 private SignedPreKeyRecord
generateSignedPreKey(IdentityKeyPair identityKeyPair
) {
416 ECKeyPair keyPair
= Curve
.generateKeyPair();
417 byte[] signature
= Curve
.calculateSignature(identityKeyPair
.getPrivateKey(), keyPair
.getPublicKey().serialize());
418 SignedPreKeyRecord
record = new SignedPreKeyRecord(account
.getNextSignedPreKeyId(), System
.currentTimeMillis(), keyPair
, signature
);
420 account
.addSignedPreKey(record);
424 } catch (InvalidKeyException e
) {
425 throw new AssertionError(e
);
429 public void verifyAccount(String verificationCode
, String pin
) throws IOException
{
430 verificationCode
= verificationCode
.replace("-", "");
431 account
.setSignalingKey(KeyUtils
.createSignalingKey());
432 // TODO make unrestricted unidentified access configurable
433 UUID uuid
= accountManager
.verifyAccountWithCode(verificationCode
, account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, pin
, null, getSelfUnidentifiedAccessKey(), false, capabilities
);
435 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
436 account
.setRegistered(true);
437 account
.setUuid(uuid
);
438 account
.setRegistrationLockPin(pin
);
444 public void setRegistrationLockPin(Optional
<String
> pin
) throws IOException
{
445 if (pin
.isPresent()) {
446 account
.setRegistrationLockPin(pin
.get());
447 throw new RuntimeException("Not implemented anymore, will be replaced with KBS");
449 account
.setRegistrationLockPin(null);
450 accountManager
.removeV1Pin();
455 private void refreshPreKeys() throws IOException
{
456 List
<PreKeyRecord
> oneTimePreKeys
= generatePreKeys();
457 final IdentityKeyPair identityKeyPair
= account
.getSignalProtocolStore().getIdentityKeyPair();
458 SignedPreKeyRecord signedPreKeyRecord
= generateSignedPreKey(identityKeyPair
);
460 accountManager
.setPreKeys(getIdentity(), signedPreKeyRecord
, oneTimePreKeys
);
463 private SignalServiceMessageReceiver
getMessageReceiver() {
464 return new SignalServiceMessageReceiver(BaseConfig
.serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), account
.getSignalingKey(), BaseConfig
.USER_AGENT
, null, timer
);
467 private SignalServiceMessageSender
getMessageSender() {
468 return new SignalServiceMessageSender(BaseConfig
.serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(),
469 account
.getDeviceId(), account
.getSignalProtocolStore(), BaseConfig
.USER_AGENT
, account
.isMultiDevice(), Optional
.fromNullable(messagePipe
), Optional
.fromNullable(unidentifiedMessagePipe
), Optional
.absent());
472 private SignalServiceProfile
getRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
) throws IOException
{
473 SignalServiceMessagePipe pipe
= unidentifiedMessagePipe
!= null && unidentifiedAccess
.isPresent() ? unidentifiedMessagePipe
478 return pipe
.getProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
479 } catch (IOException ignored
) {
483 SignalServiceMessageReceiver receiver
= getMessageReceiver();
485 return receiver
.retrieveProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
486 } catch (VerificationFailedException e
) {
487 throw new AssertionError(e
);
491 private Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(byte[] groupId
) throws IOException
{
492 File file
= getGroupAvatarFile(groupId
);
493 if (!file
.exists()) {
494 return Optional
.absent();
497 return Optional
.of(Utils
.createAttachment(file
));
500 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(String number
) throws IOException
{
501 File file
= getContactAvatarFile(number
);
502 if (!file
.exists()) {
503 return Optional
.absent();
506 return Optional
.of(Utils
.createAttachment(file
));
509 private GroupInfo
getGroupForSending(byte[] groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
510 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
512 throw new GroupNotFoundException(groupId
);
514 if (!g
.isMember(account
.getSelfAddress())) {
515 throw new NotAGroupMemberException(groupId
, g
.name
);
520 public List
<GroupInfo
> getGroups() {
521 return account
.getGroupStore().getGroups();
525 public void sendGroupMessage(String messageText
, List
<String
> attachments
,
527 throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
528 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
529 if (attachments
!= null) {
530 messageBuilder
.withAttachments(Utils
.getSignalServiceAttachments(attachments
));
532 if (groupId
!= null) {
533 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
536 messageBuilder
.asGroupMessage(group
);
539 final GroupInfo g
= getGroupForSending(groupId
);
541 messageBuilder
.withExpiration(g
.messageExpirationTime
);
543 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
546 public void sendGroupMessageReaction(String emoji
, boolean remove
, SignalServiceAddress targetAuthor
,
547 long targetSentTimestamp
, byte[] groupId
)
548 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
{
549 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, targetAuthor
, targetSentTimestamp
);
550 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
551 .withReaction(reaction
);
552 if (groupId
!= null) {
553 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
556 messageBuilder
.asGroupMessage(group
);
558 final GroupInfo g
= getGroupForSending(groupId
);
559 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
562 public void sendQuitGroupMessage(byte[] groupId
) throws GroupNotFoundException
, IOException
, EncapsulatedExceptions
{
563 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
567 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
568 .asGroupMessage(group
);
570 final GroupInfo g
= getGroupForSending(groupId
);
571 g
.removeMember(account
.getSelfAddress());
572 account
.getGroupStore().updateGroup(g
);
574 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
577 private byte[] sendUpdateGroupMessage(byte[] groupId
, String name
, Collection
<SignalServiceAddress
> members
, String avatarFile
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
579 if (groupId
== null) {
581 g
= new GroupInfo(KeyUtils
.createGroupId());
582 g
.addMembers(Collections
.singleton(account
.getSelfAddress()));
584 g
= getGroupForSending(groupId
);
591 if (members
!= null) {
592 final Set
<String
> newE164Members
= new HashSet
<>();
593 for (SignalServiceAddress member
: members
) {
594 if (g
.isMember(member
) || !member
.getNumber().isPresent()) {
597 newE164Members
.add(member
.getNumber().get());
600 final List
<ContactTokenDetails
> contacts
= accountManager
.getContacts(newE164Members
);
601 if (contacts
.size() != newE164Members
.size()) {
602 // Some of the new members are not registered on Signal
603 for (ContactTokenDetails contact
: contacts
) {
604 newE164Members
.remove(contact
.getNumber());
606 System
.err
.println("Failed to add members " + Util
.join(", ", newE164Members
) + " to group: Not registered on Signal");
607 System
.err
.println("Aborting…");
611 g
.addMembers(members
);
614 if (avatarFile
!= null) {
615 IOUtils
.createPrivateDirectories(avatarsPath
);
616 File aFile
= getGroupAvatarFile(g
.groupId
);
617 Files
.copy(Paths
.get(avatarFile
), aFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
620 account
.getGroupStore().updateGroup(g
);
622 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
624 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
628 private void sendUpdateGroupMessage(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
629 if (groupId
== null) {
632 GroupInfo g
= getGroupForSending(groupId
);
634 if (!g
.isMember(recipient
)) {
638 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
640 // Send group message only to the recipient who requested it
641 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
644 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfo g
) {
645 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
648 .withMembers(new ArrayList
<>(g
.getMembers()));
650 File aFile
= getGroupAvatarFile(g
.groupId
);
651 if (aFile
.exists()) {
653 group
.withAvatar(Utils
.createAttachment(aFile
));
654 } catch (IOException e
) {
655 throw new AttachmentInvalidException(aFile
.toString(), e
);
659 return SignalServiceDataMessage
.newBuilder()
660 .asGroupMessage(group
.build())
661 .withExpiration(g
.messageExpirationTime
);
664 private void sendGroupInfoRequest(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
665 if (groupId
== null) {
669 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
)
672 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
673 .asGroupMessage(group
.build());
675 // Send group info request message to the recipient who sent us a message with this groupId
676 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
679 private void sendReceipt(SignalServiceAddress remoteAddress
, long messageId
) throws IOException
, UntrustedIdentityException
{
680 SignalServiceReceiptMessage receiptMessage
= new SignalServiceReceiptMessage(SignalServiceReceiptMessage
.Type
.DELIVERY
,
681 Collections
.singletonList(messageId
),
682 System
.currentTimeMillis());
684 getMessageSender().sendReceipt(remoteAddress
, getAccessFor(remoteAddress
), receiptMessage
);
688 public void sendMessage(String message
, List
<String
> attachments
, String recipient
)
689 throws EncapsulatedExceptions
, AttachmentInvalidException
, IOException
, InvalidNumberException
{
690 List
<String
> recipients
= new ArrayList
<>(1);
691 recipients
.add(recipient
);
692 sendMessage(message
, attachments
, recipients
);
696 public void sendMessage(String messageText
, List
<String
> attachments
,
697 List
<String
> recipients
)
698 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
699 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
700 if (attachments
!= null) {
701 List
<SignalServiceAttachment
> attachmentStreams
= Utils
.getSignalServiceAttachments(attachments
);
703 // Upload attachments here, so we only upload once even for multiple recipients
704 SignalServiceMessageSender messageSender
= getMessageSender();
705 List
<SignalServiceAttachment
> attachmentPointers
= new ArrayList
<>(attachmentStreams
.size());
706 for (SignalServiceAttachment attachment
: attachmentStreams
) {
707 if (attachment
.isStream()) {
708 attachmentPointers
.add(messageSender
.uploadAttachment(attachment
.asStream()));
709 } else if (attachment
.isPointer()) {
710 attachmentPointers
.add(attachment
.asPointer());
714 messageBuilder
.withAttachments(attachmentPointers
);
716 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
719 public void sendMessageReaction(String emoji
, boolean remove
, SignalServiceAddress targetAuthor
,
720 long targetSentTimestamp
, List
<String
> recipients
)
721 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
722 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, targetAuthor
, targetSentTimestamp
);
723 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
724 .withReaction(reaction
);
725 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
729 public void sendEndSessionMessage(List
<String
> recipients
) throws IOException
, EncapsulatedExceptions
, InvalidNumberException
{
730 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
731 .asEndSessionMessage();
733 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
737 public String
getContactName(String number
) throws InvalidNumberException
{
738 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, account
.getUsername());
739 ContactInfo contact
= account
.getContactStore().getContact(new SignalServiceAddress(null, canonicalizedNumber
));
740 if (contact
== null) {
748 public void setContactName(String number
, String name
) throws InvalidNumberException
{
749 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, account
.getUsername());
750 final SignalServiceAddress address
= new SignalServiceAddress(null, canonicalizedNumber
);
751 ContactInfo contact
= account
.getContactStore().getContact(address
);
752 if (contact
== null) {
753 contact
= new ContactInfo(address
);
754 System
.err
.println("Add contact " + canonicalizedNumber
+ " named " + name
);
756 System
.err
.println("Updating contact " + canonicalizedNumber
+ " name " + contact
.name
+ " -> " + name
);
759 account
.getContactStore().updateContact(contact
);
764 public void setContactBlocked(String number
, boolean blocked
) throws InvalidNumberException
{
765 number
= Utils
.canonicalizeNumber(number
, account
.getUsername());
766 final SignalServiceAddress address
= new SignalServiceAddress(null, number
);
767 ContactInfo contact
= account
.getContactStore().getContact(address
);
768 if (contact
== null) {
769 contact
= new ContactInfo(address
);
770 System
.err
.println("Adding and " + (blocked ?
"blocking" : "unblocking") + " contact " + number
);
772 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " contact " + number
);
774 contact
.blocked
= blocked
;
775 account
.getContactStore().updateContact(contact
);
780 public void setGroupBlocked(final byte[] groupId
, final boolean blocked
) throws GroupNotFoundException
{
781 GroupInfo group
= getGroup(groupId
);
783 throw new GroupNotFoundException(groupId
);
785 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " group " + Base64
.encodeBytes(groupId
));
786 group
.blocked
= blocked
;
787 account
.getGroupStore().updateGroup(group
);
793 public List
<byte[]> getGroupIds() {
794 List
<GroupInfo
> groups
= getGroups();
795 List
<byte[]> ids
= new ArrayList
<>(groups
.size());
796 for (GroupInfo group
: groups
) {
797 ids
.add(group
.groupId
);
803 public String
getGroupName(byte[] groupId
) {
804 GroupInfo group
= getGroup(groupId
);
813 public List
<String
> getGroupMembers(byte[] groupId
) {
814 GroupInfo group
= getGroup(groupId
);
816 return Collections
.emptyList();
818 return new ArrayList
<>(group
.getMembersE164());
823 public byte[] updateGroup(byte[] groupId
, String name
, List
<String
> members
, String avatar
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
, InvalidNumberException
{
824 if (groupId
.length
== 0) {
827 if (name
.isEmpty()) {
830 if (members
.size() == 0) {
833 if (avatar
.isEmpty()) {
836 return sendUpdateGroupMessage(groupId
, name
, members
== null ?
null : getSignalServiceAddresses(members
), avatar
);
840 * Change the expiration timer for a contact
842 public void setExpirationTimer(SignalServiceAddress address
, int messageExpirationTimer
) {
843 ContactInfo c
= account
.getContactStore().getContact(address
);
844 c
.messageExpirationTime
= messageExpirationTimer
;
845 account
.getContactStore().updateContact(c
);
849 * Change the expiration timer for a group
851 public void setExpirationTimer(byte[] groupId
, int messageExpirationTimer
) {
852 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
853 g
.messageExpirationTime
= messageExpirationTimer
;
854 account
.getGroupStore().updateGroup(g
);
858 * Upload the sticker pack from path.
860 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
861 * @return if successful, returns the URL to install the sticker pack in the signal app
863 public String
uploadStickerPack(String path
) throws IOException
, StickerPackInvalidException
{
864 SignalServiceStickerManifestUpload manifest
= getSignalServiceStickerManifestUpload(path
);
866 SignalServiceMessageSender messageSender
= getMessageSender();
868 byte[] packKey
= KeyUtils
.createStickerUploadKey();
869 String packId
= messageSender
.uploadStickerManifest(manifest
, packKey
);
872 return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder
.encode(packId
, "utf-8") + "&pack_key=" + URLEncoder
.encode(Hex
.toStringCondensed(packKey
), "utf-8"))
874 } catch (URISyntaxException e
) {
875 throw new AssertionError(e
);
879 private SignalServiceStickerManifestUpload
getSignalServiceStickerManifestUpload(final String path
) throws IOException
, StickerPackInvalidException
{
881 String rootPath
= null;
883 final File file
= new File(path
);
884 if (file
.getName().endsWith(".zip")) {
885 zip
= new ZipFile(file
);
886 } else if (file
.getName().equals("manifest.json")) {
887 rootPath
= file
.getParent();
889 throw new StickerPackInvalidException("Could not find manifest.json");
892 JsonStickerPack pack
= parseStickerPack(rootPath
, zip
);
894 if (pack
.stickers
== null) {
895 throw new StickerPackInvalidException("Must set a 'stickers' field.");
898 if (pack
.stickers
.isEmpty()) {
899 throw new StickerPackInvalidException("Must include stickers.");
902 List
<StickerInfo
> stickers
= new ArrayList
<>(pack
.stickers
.size());
903 for (JsonStickerPack
.JsonSticker sticker
: pack
.stickers
) {
904 if (sticker
.file
== null) {
905 throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
908 Pair
<InputStream
, Long
> data
;
910 data
= getInputStreamAndLength(rootPath
, zip
, sticker
.file
);
911 } catch (IOException ignored
) {
912 throw new StickerPackInvalidException("Could not find find " + sticker
.file
);
915 StickerInfo stickerInfo
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(sticker
.emoji
).or(""));
916 stickers
.add(stickerInfo
);
919 StickerInfo cover
= null;
920 if (pack
.cover
!= null) {
921 if (pack
.cover
.file
== null) {
922 throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
925 Pair
<InputStream
, Long
> data
;
927 data
= getInputStreamAndLength(rootPath
, zip
, pack
.cover
.file
);
928 } catch (IOException ignored
) {
929 throw new StickerPackInvalidException("Could not find find " + pack
.cover
.file
);
932 cover
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(pack
.cover
.emoji
).or(""));
935 return new SignalServiceStickerManifestUpload(
942 private static JsonStickerPack
parseStickerPack(String rootPath
, ZipFile zip
) throws IOException
{
943 InputStream inputStream
;
945 inputStream
= zip
.getInputStream(zip
.getEntry("manifest.json"));
947 inputStream
= new FileInputStream((new File(rootPath
, "manifest.json")));
949 return new ObjectMapper().readValue(inputStream
, JsonStickerPack
.class);
952 private static Pair
<InputStream
, Long
> getInputStreamAndLength(final String rootPath
, final ZipFile zip
, final String subfile
) throws IOException
{
954 final ZipEntry entry
= zip
.getEntry(subfile
);
955 return new Pair
<>(zip
.getInputStream(entry
), entry
.getSize());
957 final File file
= new File(rootPath
, subfile
);
958 return new Pair
<>(new FileInputStream(file
), file
.length());
962 private void requestSyncGroups() throws IOException
{
963 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
).build();
964 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
966 sendSyncMessage(message
);
967 } catch (UntrustedIdentityException e
) {
972 private void requestSyncContacts() throws IOException
{
973 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
).build();
974 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
976 sendSyncMessage(message
);
977 } catch (UntrustedIdentityException e
) {
982 private void requestSyncBlocked() throws IOException
{
983 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
).build();
984 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
986 sendSyncMessage(message
);
987 } catch (UntrustedIdentityException e
) {
992 private void requestSyncConfiguration() throws IOException
{
993 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
).build();
994 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
996 sendSyncMessage(message
);
997 } catch (UntrustedIdentityException e
) {
1002 private byte[] getSenderCertificate() throws IOException
{
1003 byte[] certificate
= accountManager
.getSenderCertificate();
1004 // TODO cache for a day
1008 private byte[] getSelfUnidentifiedAccessKey() {
1009 return UnidentifiedAccess
.deriveAccessKeyFrom(account
.getProfileKey());
1012 private static SignalProfile
decryptProfile(SignalServiceProfile encryptedProfile
, ProfileKey profileKey
) throws IOException
{
1013 ProfileCipher profileCipher
= new ProfileCipher(profileKey
);
1015 return new SignalProfile(
1016 encryptedProfile
.getIdentityKey(),
1017 encryptedProfile
.getName() == null ?
null : new String(profileCipher
.decryptName(Base64
.decode(encryptedProfile
.getName()))),
1018 encryptedProfile
.getAvatar(),
1019 encryptedProfile
.getUnidentifiedAccess() == null || !profileCipher
.verifyUnidentifiedAccess(Base64
.decode(encryptedProfile
.getUnidentifiedAccess())) ?
null : encryptedProfile
.getUnidentifiedAccess(),
1020 encryptedProfile
.isUnrestrictedUnidentifiedAccess()
1022 } catch (InvalidCiphertextException e
) {
1027 private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient
) throws IOException
{
1028 ContactInfo contact
= account
.getContactStore().getContact(recipient
);
1029 if (contact
== null || contact
.profileKey
== null) {
1032 ProfileKey theirProfileKey
;
1034 theirProfileKey
= new ProfileKey(Base64
.decode(contact
.profileKey
));
1035 } catch (InvalidInputException e
) {
1036 throw new AssertionError(e
);
1038 SignalProfile targetProfile
= decryptProfile(getRecipientProfile(recipient
, Optional
.absent()), theirProfileKey
);
1040 if (targetProfile
== null || targetProfile
.getUnidentifiedAccess() == null) {
1044 if (targetProfile
.isUnrestrictedUnidentifiedAccess()) {
1045 return KeyUtils
.createUnrestrictedUnidentifiedAccess();
1048 return UnidentifiedAccess
.deriveAccessKeyFrom(theirProfileKey
);
1051 private Optional
<UnidentifiedAccessPair
> getAccessForSync() throws IOException
{
1052 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1053 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1055 if (selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1056 return Optional
.absent();
1060 return Optional
.of(new UnidentifiedAccessPair(
1061 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1062 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1064 } catch (InvalidCertificateException e
) {
1065 return Optional
.absent();
1069 private List
<Optional
<UnidentifiedAccessPair
>> getAccessFor(Collection
<SignalServiceAddress
> recipients
) throws IOException
{
1070 List
<Optional
<UnidentifiedAccessPair
>> result
= new ArrayList
<>(recipients
.size());
1071 for (SignalServiceAddress recipient
: recipients
) {
1072 result
.add(getAccessFor(recipient
));
1077 private Optional
<UnidentifiedAccessPair
> getAccessFor(SignalServiceAddress recipient
) throws IOException
{
1078 byte[] recipientUnidentifiedAccessKey
= getTargetUnidentifiedAccessKey(recipient
);
1079 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1080 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1082 if (recipientUnidentifiedAccessKey
== null || selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1083 return Optional
.absent();
1087 return Optional
.of(new UnidentifiedAccessPair(
1088 new UnidentifiedAccess(recipientUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1089 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1091 } catch (InvalidCertificateException e
) {
1092 return Optional
.absent();
1096 private void sendSyncMessage(SignalServiceSyncMessage message
)
1097 throws IOException
, UntrustedIdentityException
{
1098 SignalServiceMessageSender messageSender
= getMessageSender();
1100 messageSender
.sendMessage(message
, getAccessForSync());
1101 } catch (UntrustedIdentityException e
) {
1102 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1108 * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult.
1110 private void sendMessageLegacy(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1111 throws EncapsulatedExceptions
, IOException
{
1112 List
<SendMessageResult
> results
= sendMessage(messageBuilder
, recipients
);
1114 List
<UntrustedIdentityException
> untrustedIdentities
= new LinkedList
<>();
1115 List
<UnregisteredUserException
> unregisteredUsers
= new LinkedList
<>();
1116 List
<NetworkFailureException
> networkExceptions
= new LinkedList
<>();
1118 for (SendMessageResult result
: results
) {
1119 if (result
.isUnregisteredFailure()) {
1120 unregisteredUsers
.add(new UnregisteredUserException(result
.getAddress().getNumber().get(), null));
1121 } else if (result
.isNetworkFailure()) {
1122 networkExceptions
.add(new NetworkFailureException(result
.getAddress().getNumber().get(), null));
1123 } else if (result
.getIdentityFailure() != null) {
1124 untrustedIdentities
.add(new UntrustedIdentityException("Untrusted", result
.getAddress().getNumber().get(), result
.getIdentityFailure().getIdentityKey()));
1127 if (!untrustedIdentities
.isEmpty() || !unregisteredUsers
.isEmpty() || !networkExceptions
.isEmpty()) {
1128 throw new EncapsulatedExceptions(untrustedIdentities
, unregisteredUsers
, networkExceptions
);
1132 private Collection
<SignalServiceAddress
> getSignalServiceAddresses(Collection
<String
> numbers
) throws InvalidNumberException
{
1133 final Set
<SignalServiceAddress
> signalServiceAddresses
= new HashSet
<>(numbers
.size());
1134 final String username
= account
.getUsername();
1136 for (String number
: numbers
) {
1137 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, username
);
1138 if (canonicalizedNumber
.equals(username
)) {
1139 signalServiceAddresses
.add(account
.getSelfAddress());
1141 // TODO get corresponding uuid
1142 signalServiceAddresses
.add(new SignalServiceAddress(null, canonicalizedNumber
));
1145 return signalServiceAddresses
;
1148 private List
<SendMessageResult
> sendMessage(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1149 throws IOException
{
1150 if (messagePipe
== null) {
1151 messagePipe
= getMessageReceiver().createMessagePipe();
1153 if (unidentifiedMessagePipe
== null) {
1154 unidentifiedMessagePipe
= getMessageReceiver().createUnidentifiedMessagePipe();
1156 SignalServiceDataMessage message
= null;
1158 SignalServiceMessageSender messageSender
= getMessageSender();
1160 message
= messageBuilder
.build();
1161 if (message
.getGroupInfo().isPresent()) {
1163 final boolean isRecipientUpdate
= false;
1164 List
<SendMessageResult
> result
= messageSender
.sendMessage(new ArrayList
<>(recipients
), getAccessFor(recipients
), isRecipientUpdate
, message
);
1165 for (SendMessageResult r
: result
) {
1166 if (r
.getIdentityFailure() != null) {
1167 account
.getSignalProtocolStore().saveIdentity(r
.getAddress().getNumber().get(), r
.getIdentityFailure().getIdentityKey(), TrustLevel
.UNTRUSTED
);
1171 } catch (UntrustedIdentityException e
) {
1172 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1173 return Collections
.emptyList();
1175 } else if (recipients
.size() == 1 && recipients
.contains(account
.getSelfAddress())) {
1176 SignalServiceAddress recipient
= account
.getSelfAddress();
1177 final Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1178 SentTranscriptMessage transcript
= new SentTranscriptMessage(Optional
.of(recipient
),
1179 message
.getTimestamp(),
1181 message
.getExpiresInSeconds(),
1182 Collections
.singletonMap(recipient
, unidentifiedAccess
.isPresent()),
1184 SignalServiceSyncMessage syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
1186 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1188 messageSender
.sendMessage(syncMessage
, unidentifiedAccess
);
1189 } catch (UntrustedIdentityException e
) {
1190 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1191 results
.add(SendMessageResult
.identityFailure(recipient
, e
.getIdentityKey()));
1195 // Send to all individually, so sync messages are sent correctly
1196 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1197 for (SignalServiceAddress address
: recipients
) {
1198 ContactInfo contact
= account
.getContactStore().getContact(address
);
1199 if (contact
!= null) {
1200 messageBuilder
.withExpiration(contact
.messageExpirationTime
);
1201 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
1203 messageBuilder
.withExpiration(0);
1204 messageBuilder
.withProfileKey(null);
1206 message
= messageBuilder
.build();
1208 SendMessageResult result
= messageSender
.sendMessage(address
, getAccessFor(address
), message
);
1209 results
.add(result
);
1210 } catch (UntrustedIdentityException e
) {
1211 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1212 results
.add(SendMessageResult
.identityFailure(address
, e
.getIdentityKey()));
1218 if (message
!= null && message
.isEndSession()) {
1219 for (SignalServiceAddress recipient
: recipients
) {
1220 handleEndSession(recipient
.getNumber().get());
1227 private SignalServiceContent
decryptMessage(SignalServiceEnvelope envelope
) throws InvalidMetadataMessageException
, ProtocolInvalidMessageException
, ProtocolDuplicateMessageException
, ProtocolLegacyMessageException
, ProtocolInvalidKeyIdException
, InvalidMetadataVersionException
, ProtocolInvalidVersionException
, ProtocolNoSessionException
, ProtocolInvalidKeyException
, ProtocolUntrustedIdentityException
, SelfSendException
, UnsupportedDataMessageException
{
1228 SignalServiceCipher cipher
= new SignalServiceCipher(account
.getSelfAddress(), account
.getSignalProtocolStore(), Utils
.getCertificateValidator());
1230 return cipher
.decrypt(envelope
);
1231 } catch (ProtocolUntrustedIdentityException e
) {
1232 // TODO We don't get the new untrusted identity from ProtocolUntrustedIdentityException anymore ... we need to get it from somewhere else
1233 // account.getSignalProtocolStore().saveIdentity(e.getSender(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
1238 private void handleEndSession(String source
) {
1239 account
.getSignalProtocolStore().deleteAllSessions(source
);
1242 private void handleSignalServiceDataMessage(SignalServiceDataMessage message
, boolean isSync
, SignalServiceAddress source
, SignalServiceAddress destination
, boolean ignoreAttachments
) {
1243 if (message
.getGroupInfo().isPresent()) {
1244 SignalServiceGroup groupInfo
= message
.getGroupInfo().get();
1245 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1246 switch (groupInfo
.getType()) {
1248 if (group
== null) {
1249 group
= new GroupInfo(groupInfo
.getGroupId());
1252 if (groupInfo
.getAvatar().isPresent()) {
1253 SignalServiceAttachment avatar
= groupInfo
.getAvatar().get();
1254 if (avatar
.isPointer()) {
1256 retrieveGroupAvatarAttachment(avatar
.asPointer(), group
.groupId
);
1257 } catch (IOException
| InvalidMessageException e
) {
1258 System
.err
.println("Failed to retrieve group avatar (" + avatar
.asPointer().getId() + "): " + e
.getMessage());
1263 if (groupInfo
.getName().isPresent()) {
1264 group
.name
= groupInfo
.getName().get();
1267 if (groupInfo
.getMembers().isPresent()) {
1268 group
.addMembers(groupInfo
.getMembers().get());
1271 account
.getGroupStore().updateGroup(group
);
1274 if (group
== null) {
1276 sendGroupInfoRequest(groupInfo
.getGroupId(), source
);
1277 } catch (IOException
| EncapsulatedExceptions e
) {
1278 e
.printStackTrace();
1283 if (group
== null) {
1285 sendGroupInfoRequest(groupInfo
.getGroupId(), source
);
1286 } catch (IOException
| EncapsulatedExceptions e
) {
1287 e
.printStackTrace();
1290 group
.removeMember(source
);
1291 account
.getGroupStore().updateGroup(group
);
1295 if (group
!= null) {
1297 sendUpdateGroupMessage(groupInfo
.getGroupId(), source
);
1298 } catch (IOException
| EncapsulatedExceptions e
) {
1299 e
.printStackTrace();
1300 } catch (NotAGroupMemberException e
) {
1301 // We have left this group, so don't send a group update message
1307 if (message
.isEndSession()) {
1308 handleEndSession(isSync ? destination
.getNumber().get() : source
.getNumber().get());
1310 if (message
.isExpirationUpdate() || message
.getBody().isPresent()) {
1311 if (message
.getGroupInfo().isPresent()) {
1312 SignalServiceGroup groupInfo
= message
.getGroupInfo().get();
1313 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1314 if (group
== null) {
1315 group
= new GroupInfo(groupInfo
.getGroupId());
1317 if (group
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1318 group
.messageExpirationTime
= message
.getExpiresInSeconds();
1319 account
.getGroupStore().updateGroup(group
);
1322 ContactInfo contact
= account
.getContactStore().getContact(isSync ? destination
: source
);
1323 if (contact
== null) {
1324 contact
= new ContactInfo(isSync ? destination
: source
);
1326 if (contact
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1327 contact
.messageExpirationTime
= message
.getExpiresInSeconds();
1328 account
.getContactStore().updateContact(contact
);
1332 if (message
.getAttachments().isPresent() && !ignoreAttachments
) {
1333 for (SignalServiceAttachment attachment
: message
.getAttachments().get()) {
1334 if (attachment
.isPointer()) {
1336 retrieveAttachment(attachment
.asPointer());
1337 } catch (IOException
| InvalidMessageException e
) {
1338 System
.err
.println("Failed to retrieve attachment (" + attachment
.asPointer().getId() + "): " + e
.getMessage());
1343 if (message
.getProfileKey().isPresent() && message
.getProfileKey().get().length
== 32) {
1344 if (source
.matches(account
.getSelfAddress())) {
1346 this.account
.setProfileKey(new ProfileKey(message
.getProfileKey().get()));
1347 } catch (InvalidInputException ignored
) {
1349 ContactInfo contact
= account
.getContactStore().getContact(source
);
1350 if (contact
!= null) {
1351 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1352 account
.getContactStore().updateContact(contact
);
1355 ContactInfo contact
= account
.getContactStore().getContact(source
);
1356 if (contact
== null) {
1357 contact
= new ContactInfo(source
);
1359 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1360 account
.getContactStore().updateContact(contact
);
1363 if (message
.getPreviews().isPresent()) {
1364 final List
<SignalServiceDataMessage
.Preview
> previews
= message
.getPreviews().get();
1365 for (SignalServiceDataMessage
.Preview preview
: previews
) {
1366 if (preview
.getImage().isPresent() && preview
.getImage().get().isPointer()) {
1367 SignalServiceAttachmentPointer attachment
= preview
.getImage().get().asPointer();
1369 retrieveAttachment(attachment
);
1370 } catch (IOException
| InvalidMessageException e
) {
1371 System
.err
.println("Failed to retrieve attachment (" + attachment
.getId() + "): " + e
.getMessage());
1378 private void retryFailedReceivedMessages(ReceiveMessageHandler handler
, boolean ignoreAttachments
) {
1379 final File cachePath
= new File(getMessageCachePath());
1380 if (!cachePath
.exists()) {
1383 for (final File dir
: Objects
.requireNonNull(cachePath
.listFiles())) {
1384 if (!dir
.isDirectory()) {
1388 for (final File fileEntry
: Objects
.requireNonNull(dir
.listFiles())) {
1389 if (!fileEntry
.isFile()) {
1392 SignalServiceEnvelope envelope
;
1394 envelope
= Utils
.loadEnvelope(fileEntry
);
1395 if (envelope
== null) {
1398 } catch (IOException e
) {
1399 e
.printStackTrace();
1402 SignalServiceContent content
= null;
1403 if (!envelope
.isReceipt()) {
1405 content
= decryptMessage(envelope
);
1406 } catch (Exception e
) {
1409 handleMessage(envelope
, content
, ignoreAttachments
);
1412 handler
.handleMessage(envelope
, content
, null);
1414 Files
.delete(fileEntry
.toPath());
1415 } catch (IOException e
) {
1416 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1419 // Try to delete directory if empty
1424 public void receiveMessages(long timeout
, TimeUnit unit
, boolean returnOnTimeout
, boolean ignoreAttachments
, ReceiveMessageHandler handler
) throws IOException
{
1425 retryFailedReceivedMessages(handler
, ignoreAttachments
);
1426 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1429 if (messagePipe
== null) {
1430 messagePipe
= messageReceiver
.createMessagePipe();
1434 SignalServiceEnvelope envelope
;
1435 SignalServiceContent content
= null;
1436 Exception exception
= null;
1437 final long now
= new Date().getTime();
1439 envelope
= messagePipe
.read(timeout
, unit
, envelope1
-> {
1440 // store message on disk, before acknowledging receipt to the server
1442 File cacheFile
= getMessageCacheFile(envelope1
.getSourceE164().get(), now
, envelope1
.getTimestamp());
1443 Utils
.storeEnvelope(envelope1
, cacheFile
);
1444 } catch (IOException e
) {
1445 System
.err
.println("Failed to store encrypted message in disk cache, ignoring: " + e
.getMessage());
1448 } catch (TimeoutException e
) {
1449 if (returnOnTimeout
)
1452 } catch (InvalidVersionException e
) {
1453 System
.err
.println("Ignoring error: " + e
.getMessage());
1456 if (!envelope
.isReceipt()) {
1458 content
= decryptMessage(envelope
);
1459 } catch (Exception e
) {
1462 handleMessage(envelope
, content
, ignoreAttachments
);
1465 if (!isMessageBlocked(envelope
, content
)) {
1466 handler
.handleMessage(envelope
, content
, exception
);
1468 if (!(exception
instanceof ProtocolUntrustedIdentityException
)) {
1469 File cacheFile
= null;
1471 cacheFile
= getMessageCacheFile(envelope
.getSourceE164().get(), now
, envelope
.getTimestamp());
1472 Files
.delete(cacheFile
.toPath());
1473 // Try to delete directory if empty
1474 new File(getMessageCachePath()).delete();
1475 } catch (IOException e
) {
1476 System
.err
.println("Failed to delete cached message file “" + cacheFile
+ "”: " + e
.getMessage());
1481 if (messagePipe
!= null) {
1482 messagePipe
.shutdown();
1488 private boolean isMessageBlocked(SignalServiceEnvelope envelope
, SignalServiceContent content
) {
1489 SignalServiceAddress source
;
1490 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1491 source
= envelope
.getSourceAddress();
1492 } else if (content
!= null) {
1493 source
= content
.getSender();
1497 ContactInfo sourceContact
= getContact(source
.getNumber().get());
1498 if (sourceContact
!= null && sourceContact
.blocked
) {
1502 if (content
!= null && content
.getDataMessage().isPresent()) {
1503 SignalServiceDataMessage message
= content
.getDataMessage().get();
1504 if (message
.getGroupInfo().isPresent()) {
1505 SignalServiceGroup groupInfo
= message
.getGroupInfo().get();
1506 GroupInfo group
= getGroup(groupInfo
.getGroupId());
1507 if (groupInfo
.getType() == SignalServiceGroup
.Type
.DELIVER
&& group
!= null && group
.blocked
) {
1515 private void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent content
, boolean ignoreAttachments
) {
1516 if (content
!= null) {
1517 SignalServiceAddress sender
;
1518 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1519 sender
= envelope
.getSourceAddress();
1521 sender
= content
.getSender();
1523 if (content
.getDataMessage().isPresent()) {
1524 SignalServiceDataMessage message
= content
.getDataMessage().get();
1526 if (content
.isNeedsReceipt()) {
1528 sendReceipt(sender
, message
.getTimestamp());
1529 } catch (IOException
| UntrustedIdentityException e
) {
1530 e
.printStackTrace();
1534 handleSignalServiceDataMessage(message
, false, sender
, account
.getSelfAddress(), ignoreAttachments
);
1536 if (content
.getSyncMessage().isPresent()) {
1537 account
.setMultiDevice(true);
1538 SignalServiceSyncMessage syncMessage
= content
.getSyncMessage().get();
1539 if (syncMessage
.getSent().isPresent()) {
1540 SentTranscriptMessage message
= syncMessage
.getSent().get();
1541 handleSignalServiceDataMessage(message
.getMessage(), true, sender
, message
.getDestination().orNull(), ignoreAttachments
);
1543 if (syncMessage
.getRequest().isPresent()) {
1544 RequestMessage rm
= syncMessage
.getRequest().get();
1545 if (rm
.isContactsRequest()) {
1548 } catch (UntrustedIdentityException
| IOException e
) {
1549 e
.printStackTrace();
1552 if (rm
.isGroupsRequest()) {
1555 } catch (UntrustedIdentityException
| IOException e
) {
1556 e
.printStackTrace();
1559 if (rm
.isBlockedListRequest()) {
1562 } catch (UntrustedIdentityException
| IOException e
) {
1563 e
.printStackTrace();
1566 // TODO Handle rm.isConfigurationRequest();
1568 if (syncMessage
.getGroups().isPresent()) {
1569 File tmpFile
= null;
1571 tmpFile
= IOUtils
.createTempFile();
1572 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(syncMessage
.getGroups().get().asPointer(), tmpFile
)) {
1573 DeviceGroupsInputStream s
= new DeviceGroupsInputStream(attachmentAsStream
);
1575 while ((g
= s
.read()) != null) {
1576 GroupInfo syncGroup
= account
.getGroupStore().getGroup(g
.getId());
1577 if (syncGroup
== null) {
1578 syncGroup
= new GroupInfo(g
.getId());
1580 if (g
.getName().isPresent()) {
1581 syncGroup
.name
= g
.getName().get();
1583 syncGroup
.addMembers(g
.getMembers());
1584 if (!g
.isActive()) {
1585 syncGroup
.removeMember(account
.getSelfAddress());
1587 // Add ourself to the member set as it's marked as active
1588 syncGroup
.addMembers(Collections
.singleton(account
.getSelfAddress()));
1590 syncGroup
.blocked
= g
.isBlocked();
1591 if (g
.getColor().isPresent()) {
1592 syncGroup
.color
= g
.getColor().get();
1595 if (g
.getAvatar().isPresent()) {
1596 retrieveGroupAvatarAttachment(g
.getAvatar().get(), syncGroup
.groupId
);
1598 syncGroup
.inboxPosition
= g
.getInboxPosition().orNull();
1599 syncGroup
.archived
= g
.isArchived();
1600 account
.getGroupStore().updateGroup(syncGroup
);
1603 } catch (Exception e
) {
1604 e
.printStackTrace();
1606 if (tmpFile
!= null) {
1608 Files
.delete(tmpFile
.toPath());
1609 } catch (IOException e
) {
1610 System
.err
.println("Failed to delete received groups temp file “" + tmpFile
+ "”: " + e
.getMessage());
1615 if (syncMessage
.getBlockedList().isPresent()) {
1616 final BlockedListMessage blockedListMessage
= syncMessage
.getBlockedList().get();
1617 for (SignalServiceAddress address
: blockedListMessage
.getAddresses()) {
1618 if (address
.getNumber().isPresent()) {
1620 setContactBlocked(address
.getNumber().get(), true);
1621 } catch (InvalidNumberException e
) {
1622 e
.printStackTrace();
1626 for (byte[] groupId
: blockedListMessage
.getGroupIds()) {
1628 setGroupBlocked(groupId
, true);
1629 } catch (GroupNotFoundException e
) {
1630 System
.err
.println("BlockedListMessage contained groupID that was not found in GroupStore: " + Base64
.encodeBytes(groupId
));
1634 if (syncMessage
.getContacts().isPresent()) {
1635 File tmpFile
= null;
1637 tmpFile
= IOUtils
.createTempFile();
1638 final ContactsMessage contactsMessage
= syncMessage
.getContacts().get();
1639 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(contactsMessage
.getContactsStream().asPointer(), tmpFile
)) {
1640 DeviceContactsInputStream s
= new DeviceContactsInputStream(attachmentAsStream
);
1641 if (contactsMessage
.isComplete()) {
1642 account
.getContactStore().clear();
1645 while ((c
= s
.read()) != null) {
1646 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
1647 account
.setProfileKey(c
.getProfileKey().get());
1649 ContactInfo contact
= account
.getContactStore().getContact(c
.getAddress());
1650 if (contact
== null) {
1651 contact
= new ContactInfo(c
.getAddress());
1653 if (c
.getName().isPresent()) {
1654 contact
.name
= c
.getName().get();
1656 if (c
.getColor().isPresent()) {
1657 contact
.color
= c
.getColor().get();
1659 if (c
.getProfileKey().isPresent()) {
1660 contact
.profileKey
= Base64
.encodeBytes(c
.getProfileKey().get().serialize());
1662 if (c
.getVerified().isPresent()) {
1663 final VerifiedMessage verifiedMessage
= c
.getVerified().get();
1664 account
.getSignalProtocolStore().saveIdentity(verifiedMessage
.getDestination().getNumber().get(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1666 if (c
.getExpirationTimer().isPresent()) {
1667 contact
.messageExpirationTime
= c
.getExpirationTimer().get();
1669 contact
.blocked
= c
.isBlocked();
1670 contact
.inboxPosition
= c
.getInboxPosition().orNull();
1671 contact
.archived
= c
.isArchived();
1672 account
.getContactStore().updateContact(contact
);
1674 if (c
.getAvatar().isPresent()) {
1675 retrieveContactAvatarAttachment(c
.getAvatar().get(), contact
.number
);
1679 } catch (Exception e
) {
1680 e
.printStackTrace();
1682 if (tmpFile
!= null) {
1684 Files
.delete(tmpFile
.toPath());
1685 } catch (IOException e
) {
1686 System
.err
.println("Failed to delete received contacts temp file “" + tmpFile
+ "”: " + e
.getMessage());
1691 if (syncMessage
.getVerified().isPresent()) {
1692 final VerifiedMessage verifiedMessage
= syncMessage
.getVerified().get();
1693 account
.getSignalProtocolStore().saveIdentity(verifiedMessage
.getDestination().getNumber().get(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1695 if (syncMessage
.getConfiguration().isPresent()) {
1702 private File
getContactAvatarFile(String number
) {
1703 return new File(avatarsPath
, "contact-" + number
);
1706 private File
retrieveContactAvatarAttachment(SignalServiceAttachment attachment
, String number
) throws IOException
, InvalidMessageException
{
1707 IOUtils
.createPrivateDirectories(avatarsPath
);
1708 if (attachment
.isPointer()) {
1709 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1710 return retrieveAttachment(pointer
, getContactAvatarFile(number
), false);
1712 SignalServiceAttachmentStream stream
= attachment
.asStream();
1713 return Utils
.retrieveAttachment(stream
, getContactAvatarFile(number
));
1717 private File
getGroupAvatarFile(byte[] groupId
) {
1718 return new File(avatarsPath
, "group-" + Base64
.encodeBytes(groupId
).replace("/", "_"));
1721 private File
retrieveGroupAvatarAttachment(SignalServiceAttachment attachment
, byte[] groupId
) throws IOException
, InvalidMessageException
{
1722 IOUtils
.createPrivateDirectories(avatarsPath
);
1723 if (attachment
.isPointer()) {
1724 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1725 return retrieveAttachment(pointer
, getGroupAvatarFile(groupId
), false);
1727 SignalServiceAttachmentStream stream
= attachment
.asStream();
1728 return Utils
.retrieveAttachment(stream
, getGroupAvatarFile(groupId
));
1732 public File
getAttachmentFile(long attachmentId
) {
1733 return new File(attachmentsPath
, attachmentId
+ "");
1736 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
) throws IOException
, InvalidMessageException
{
1737 IOUtils
.createPrivateDirectories(attachmentsPath
);
1738 return retrieveAttachment(pointer
, getAttachmentFile(pointer
.getId()), true);
1741 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
, File outputFile
, boolean storePreview
) throws IOException
, InvalidMessageException
{
1742 if (storePreview
&& pointer
.getPreview().isPresent()) {
1743 File previewFile
= new File(outputFile
+ ".preview");
1744 try (OutputStream output
= new FileOutputStream(previewFile
)) {
1745 byte[] preview
= pointer
.getPreview().get();
1746 output
.write(preview
, 0, preview
.length
);
1747 } catch (FileNotFoundException e
) {
1748 e
.printStackTrace();
1753 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1755 File tmpFile
= IOUtils
.createTempFile();
1756 try (InputStream input
= messageReceiver
.retrieveAttachment(pointer
, tmpFile
, BaseConfig
.MAX_ATTACHMENT_SIZE
)) {
1757 try (OutputStream output
= new FileOutputStream(outputFile
)) {
1758 byte[] buffer
= new byte[4096];
1761 while ((read
= input
.read(buffer
)) != -1) {
1762 output
.write(buffer
, 0, read
);
1764 } catch (FileNotFoundException e
) {
1765 e
.printStackTrace();
1770 Files
.delete(tmpFile
.toPath());
1771 } catch (IOException e
) {
1772 System
.err
.println("Failed to delete received attachment temp file “" + tmpFile
+ "”: " + e
.getMessage());
1778 private InputStream
retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer
, File tmpFile
) throws IOException
, InvalidMessageException
{
1779 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1780 return messageReceiver
.retrieveAttachment(pointer
, tmpFile
, BaseConfig
.MAX_ATTACHMENT_SIZE
);
1784 public boolean isRemote() {
1788 private void sendGroups() throws IOException
, UntrustedIdentityException
{
1789 File groupsFile
= IOUtils
.createTempFile();
1792 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
1793 DeviceGroupsOutputStream out
= new DeviceGroupsOutputStream(fos
);
1794 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1795 out
.write(new DeviceGroup(record.groupId
, Optional
.fromNullable(record.name
),
1796 new ArrayList
<>(record.getMembers()), createGroupAvatarAttachment(record.groupId
),
1797 record.isMember(account
.getSelfAddress()), Optional
.of(record.messageExpirationTime
),
1798 Optional
.fromNullable(record.color
), record.blocked
, Optional
.fromNullable(record.inboxPosition
), record.archived
));
1802 if (groupsFile
.exists() && groupsFile
.length() > 0) {
1803 try (FileInputStream groupsFileStream
= new FileInputStream(groupsFile
)) {
1804 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1805 .withStream(groupsFileStream
)
1806 .withContentType("application/octet-stream")
1807 .withLength(groupsFile
.length())
1810 sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
1815 Files
.delete(groupsFile
.toPath());
1816 } catch (IOException e
) {
1817 System
.err
.println("Failed to delete groups temp file “" + groupsFile
+ "”: " + e
.getMessage());
1822 public void sendContacts() throws IOException
, UntrustedIdentityException
{
1823 File contactsFile
= IOUtils
.createTempFile();
1826 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
1827 DeviceContactsOutputStream out
= new DeviceContactsOutputStream(fos
);
1828 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1829 VerifiedMessage verifiedMessage
= null;
1830 if (getIdentities().containsKey(record.number
)) {
1831 JsonIdentityKeyStore
.Identity currentIdentity
= null;
1832 for (JsonIdentityKeyStore
.Identity id
: getIdentities().get(record.number
)) {
1833 if (currentIdentity
== null || id
.getDateAdded().after(currentIdentity
.getDateAdded())) {
1834 currentIdentity
= id
;
1837 if (currentIdentity
!= null) {
1838 verifiedMessage
= new VerifiedMessage(record.getAddress(), currentIdentity
.getIdentityKey(), currentIdentity
.getTrustLevel().toVerifiedState(), currentIdentity
.getDateAdded().getTime());
1842 ProfileKey profileKey
= null;
1844 profileKey
= record.profileKey
== null ?
null : new ProfileKey(Base64
.decode(record.profileKey
));
1845 } catch (InvalidInputException ignored
) {
1847 out
.write(new DeviceContact(record.getAddress(), Optional
.fromNullable(record.name
),
1848 createContactAvatarAttachment(record.number
), Optional
.fromNullable(record.color
),
1849 Optional
.fromNullable(verifiedMessage
), Optional
.fromNullable(profileKey
), record.blocked
,
1850 Optional
.of(record.messageExpirationTime
),
1851 Optional
.fromNullable(record.inboxPosition
), record.archived
));
1854 if (account
.getProfileKey() != null) {
1855 // Send our own profile key as well
1856 out
.write(new DeviceContact(account
.getSelfAddress(),
1857 Optional
.absent(), Optional
.absent(),
1858 Optional
.absent(), Optional
.absent(),
1859 Optional
.of(account
.getProfileKey()),
1860 false, Optional
.absent(), Optional
.absent(), false));
1864 if (contactsFile
.exists() && contactsFile
.length() > 0) {
1865 try (FileInputStream contactsFileStream
= new FileInputStream(contactsFile
)) {
1866 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1867 .withStream(contactsFileStream
)
1868 .withContentType("application/octet-stream")
1869 .withLength(contactsFile
.length())
1872 sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
, true)));
1877 Files
.delete(contactsFile
.toPath());
1878 } catch (IOException e
) {
1879 System
.err
.println("Failed to delete contacts temp file “" + contactsFile
+ "”: " + e
.getMessage());
1884 private void sendBlockedList() throws IOException
, UntrustedIdentityException
{
1885 List
<SignalServiceAddress
> addresses
= new ArrayList
<>();
1886 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1887 if (record.blocked
) {
1888 addresses
.add(record.getAddress());
1891 List
<byte[]> groupIds
= new ArrayList
<>();
1892 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1893 if (record.blocked
) {
1894 groupIds
.add(record.groupId
);
1897 sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
1900 private void sendVerifiedMessage(SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
) throws IOException
, UntrustedIdentityException
{
1901 VerifiedMessage verifiedMessage
= new VerifiedMessage(destination
, identityKey
, trustLevel
.toVerifiedState(), System
.currentTimeMillis());
1902 sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
1905 public List
<ContactInfo
> getContacts() {
1906 return account
.getContactStore().getContacts();
1909 public ContactInfo
getContact(String number
) {
1910 return account
.getContactStore().getContact(new SignalServiceAddress(null, number
));
1913 public GroupInfo
getGroup(byte[] groupId
) {
1914 return account
.getGroupStore().getGroup(groupId
);
1917 public Map
<String
, List
<JsonIdentityKeyStore
.Identity
>> getIdentities() {
1918 return account
.getSignalProtocolStore().getIdentities();
1921 public Pair
<String
, List
<JsonIdentityKeyStore
.Identity
>> getIdentities(String number
) throws InvalidNumberException
{
1922 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, account
.getUsername());
1923 return new Pair
<>(canonicalizedNumber
, account
.getSignalProtocolStore().getIdentities(canonicalizedNumber
));
1927 * Trust this the identity with this fingerprint
1929 * @param name username of the identity
1930 * @param fingerprint Fingerprint
1932 public boolean trustIdentityVerified(String name
, byte[] fingerprint
) {
1933 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1937 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1938 if (!Arrays
.equals(id
.getIdentityKey().serialize(), fingerprint
)) {
1942 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1944 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1945 } catch (IOException
| UntrustedIdentityException e
) {
1946 e
.printStackTrace();
1955 * Trust this the identity with this safety number
1957 * @param name username of the identity
1958 * @param safetyNumber Safety number
1960 public boolean trustIdentityVerifiedSafetyNumber(String name
, String safetyNumber
) {
1961 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1965 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1966 if (!safetyNumber
.equals(computeSafetyNumber(name
, id
.getIdentityKey()))) {
1970 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1972 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1973 } catch (IOException
| UntrustedIdentityException e
) {
1974 e
.printStackTrace();
1983 * Trust all keys of this identity without verification
1985 * @param name username of the identity
1987 public boolean trustIdentityAllKeys(String name
) {
1988 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1992 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1993 if (id
.getTrustLevel() == TrustLevel
.UNTRUSTED
) {
1994 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
1996 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
1997 } catch (IOException
| UntrustedIdentityException e
) {
1998 e
.printStackTrace();
2006 public String
computeSafetyNumber(String theirUsername
, IdentityKey theirIdentityKey
) {
2007 return Utils
.computeSafetyNumber(account
.getUsername(), getIdentity(), theirUsername
, theirIdentityKey
);
2010 public interface ReceiveMessageHandler
{
2012 void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent decryptedContent
, Throwable e
);