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
.storage
.SignalAccount
;
28 import org
.asamk
.signal
.storage
.contacts
.ContactInfo
;
29 import org
.asamk
.signal
.storage
.groups
.GroupInfo
;
30 import org
.asamk
.signal
.storage
.groups
.JsonGroupStore
;
31 import org
.asamk
.signal
.storage
.protocol
.JsonIdentityKeyStore
;
32 import org
.asamk
.signal
.util
.IOUtils
;
33 import org
.asamk
.signal
.util
.Util
;
34 import org
.signal
.libsignal
.metadata
.InvalidMetadataMessageException
;
35 import org
.signal
.libsignal
.metadata
.InvalidMetadataVersionException
;
36 import org
.signal
.libsignal
.metadata
.ProtocolDuplicateMessageException
;
37 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyException
;
38 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyIdException
;
39 import org
.signal
.libsignal
.metadata
.ProtocolInvalidMessageException
;
40 import org
.signal
.libsignal
.metadata
.ProtocolInvalidVersionException
;
41 import org
.signal
.libsignal
.metadata
.ProtocolLegacyMessageException
;
42 import org
.signal
.libsignal
.metadata
.ProtocolNoSessionException
;
43 import org
.signal
.libsignal
.metadata
.ProtocolUntrustedIdentityException
;
44 import org
.signal
.libsignal
.metadata
.SelfSendException
;
45 import org
.signal
.libsignal
.metadata
.certificate
.InvalidCertificateException
;
46 import org
.signal
.zkgroup
.InvalidInputException
;
47 import org
.signal
.zkgroup
.VerificationFailedException
;
48 import org
.signal
.zkgroup
.profiles
.ClientZkProfileOperations
;
49 import org
.signal
.zkgroup
.profiles
.ProfileKey
;
50 import org
.whispersystems
.libsignal
.IdentityKey
;
51 import org
.whispersystems
.libsignal
.IdentityKeyPair
;
52 import org
.whispersystems
.libsignal
.InvalidKeyException
;
53 import org
.whispersystems
.libsignal
.InvalidMessageException
;
54 import org
.whispersystems
.libsignal
.InvalidVersionException
;
55 import org
.whispersystems
.libsignal
.ecc
.Curve
;
56 import org
.whispersystems
.libsignal
.ecc
.ECKeyPair
;
57 import org
.whispersystems
.libsignal
.ecc
.ECPublicKey
;
58 import org
.whispersystems
.libsignal
.state
.PreKeyRecord
;
59 import org
.whispersystems
.libsignal
.state
.SignedPreKeyRecord
;
60 import org
.whispersystems
.libsignal
.util
.KeyHelper
;
61 import org
.whispersystems
.libsignal
.util
.Medium
;
62 import org
.whispersystems
.libsignal
.util
.Pair
;
63 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
64 import org
.whispersystems
.signalservice
.api
.SignalServiceAccountManager
;
65 import org
.whispersystems
.signalservice
.api
.SignalServiceMessagePipe
;
66 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageReceiver
;
67 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageSender
;
68 import org
.whispersystems
.signalservice
.api
.crypto
.InvalidCiphertextException
;
69 import org
.whispersystems
.signalservice
.api
.crypto
.ProfileCipher
;
70 import org
.whispersystems
.signalservice
.api
.crypto
.SignalServiceCipher
;
71 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccess
;
72 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccessPair
;
73 import org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException
;
74 import org
.whispersystems
.signalservice
.api
.messages
.SendMessageResult
;
75 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachment
;
76 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentPointer
;
77 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentRemoteId
;
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
.EncapsulatedExceptions
;
103 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.MissingConfigurationException
;
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
.api
.util
.UuidUtil
;
111 import org
.whispersystems
.signalservice
.internal
.configuration
.SignalServiceConfiguration
;
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
.ArrayList
;
132 import java
.util
.Arrays
;
133 import java
.util
.Collection
;
134 import java
.util
.Collections
;
135 import java
.util
.Date
;
136 import java
.util
.HashSet
;
137 import java
.util
.LinkedList
;
138 import java
.util
.List
;
139 import java
.util
.Locale
;
140 import java
.util
.Objects
;
141 import java
.util
.Set
;
142 import java
.util
.UUID
;
143 import java
.util
.concurrent
.TimeUnit
;
144 import java
.util
.concurrent
.TimeoutException
;
145 import java
.util
.stream
.Collectors
;
146 import java
.util
.zip
.ZipEntry
;
147 import java
.util
.zip
.ZipFile
;
149 public class Manager
implements Signal
{
151 private final SleepTimer timer
= new UptimeSleepTimer();
152 private final SignalServiceConfiguration serviceConfiguration
;
153 private final String userAgent
;
155 private final SignalAccount account
;
156 private final PathConfig pathConfig
;
157 private SignalServiceAccountManager accountManager
;
158 private SignalServiceMessagePipe messagePipe
= null;
159 private SignalServiceMessagePipe unidentifiedMessagePipe
= null;
161 public Manager(SignalAccount account
, PathConfig pathConfig
, SignalServiceConfiguration serviceConfiguration
, String userAgent
) {
162 this.account
= account
;
163 this.pathConfig
= pathConfig
;
164 this.serviceConfiguration
= serviceConfiguration
;
165 this.userAgent
= userAgent
;
166 this.accountManager
= createSignalServiceAccountManager();
168 this.account
.setResolver(this::resolveSignalServiceAddress
);
171 public String
getUsername() {
172 return account
.getUsername();
175 public SignalServiceAddress
getSelfAddress() {
176 return account
.getSelfAddress();
179 private SignalServiceAccountManager
createSignalServiceAccountManager() {
180 return new SignalServiceAccountManager(serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), userAgent
, timer
);
183 private IdentityKeyPair
getIdentityKeyPair() {
184 return account
.getSignalProtocolStore().getIdentityKeyPair();
187 public int getDeviceId() {
188 return account
.getDeviceId();
191 private String
getMessageCachePath() {
192 return pathConfig
.getDataPath() + "/" + account
.getUsername() + ".d/msg-cache";
195 private String
getMessageCachePath(String sender
) {
196 if (sender
== null || sender
.isEmpty()) {
197 return getMessageCachePath();
200 return getMessageCachePath() + "/" + sender
.replace("/", "_");
203 private File
getMessageCacheFile(String sender
, long now
, long timestamp
) throws IOException
{
204 String cachePath
= getMessageCachePath(sender
);
205 IOUtils
.createPrivateDirectories(cachePath
);
206 return new File(cachePath
+ "/" + now
+ "_" + timestamp
);
209 public static Manager
init(String username
, String settingsPath
, SignalServiceConfiguration serviceConfiguration
, String userAgent
) throws IOException
{
210 PathConfig pathConfig
= PathConfig
.createDefault(settingsPath
);
212 if (!SignalAccount
.userExists(pathConfig
.getDataPath(), username
)) {
213 IdentityKeyPair identityKey
= KeyHelper
.generateIdentityKeyPair();
214 int registrationId
= KeyHelper
.generateRegistrationId(false);
216 ProfileKey profileKey
= KeyUtils
.createProfileKey();
217 SignalAccount account
= SignalAccount
.create(pathConfig
.getDataPath(), username
, identityKey
, registrationId
, profileKey
);
220 return new Manager(account
, pathConfig
, serviceConfiguration
, userAgent
);
223 SignalAccount account
= SignalAccount
.load(pathConfig
.getDataPath(), username
);
225 Manager m
= new Manager(account
, pathConfig
, serviceConfiguration
, userAgent
);
227 m
.migrateLegacyConfigs();
228 m
.checkAccountState();
233 private void migrateLegacyConfigs() {
234 // Copy group avatars that were previously stored in the attachments folder
235 // to the new avatar folder
236 if (JsonGroupStore
.groupsWithLegacyAvatarId
.size() > 0) {
237 for (GroupInfo g
: JsonGroupStore
.groupsWithLegacyAvatarId
) {
238 File avatarFile
= getGroupAvatarFile(g
.groupId
);
239 File attachmentFile
= getAttachmentFile(new SignalServiceAttachmentRemoteId(g
.getAvatarId()));
240 if (!avatarFile
.exists() && attachmentFile
.exists()) {
242 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
243 Files
.copy(attachmentFile
.toPath(), avatarFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
244 } catch (Exception e
) {
249 JsonGroupStore
.groupsWithLegacyAvatarId
.clear();
252 if (account
.getProfileKey() == null) {
253 // Old config file, creating new profile key
254 account
.setProfileKey(KeyUtils
.createProfileKey());
259 private void checkAccountState() throws IOException
{
260 if (account
.isRegistered()) {
261 if (accountManager
.getPreKeysCount() < ServiceConfig
.PREKEY_MINIMUM_COUNT
) {
265 if (account
.getUuid() == null) {
266 account
.setUuid(accountManager
.getOwnUuid());
272 public boolean isRegistered() {
273 return account
.isRegistered();
276 public void register(boolean voiceVerification
) throws IOException
{
277 account
.setPassword(KeyUtils
.createPassword());
279 // Resetting UUID, because registering doesn't work otherwise
280 account
.setUuid(null);
281 accountManager
= createSignalServiceAccountManager();
283 if (voiceVerification
) {
284 accountManager
.requestVoiceVerificationCode(Locale
.getDefault(), Optional
.absent(), Optional
.absent());
286 accountManager
.requestSmsVerificationCode(false, Optional
.absent(), Optional
.absent());
289 account
.setRegistered(false);
293 public void updateAccountAttributes() throws IOException
{
294 accountManager
.setAccountAttributes(account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, account
.getRegistrationLockPin(), account
.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, ServiceConfig
.capabilities
);
297 public void setProfileName(String name
) throws IOException
{
298 accountManager
.setProfileName(account
.getProfileKey(), name
);
301 public void setProfileAvatar(File avatar
) throws IOException
{
302 final StreamDetails streamDetails
= Utils
.createStreamDetailsFromFile(avatar
);
303 accountManager
.setProfileAvatar(account
.getProfileKey(), streamDetails
);
304 streamDetails
.getStream().close();
307 public void removeProfileAvatar() throws IOException
{
308 accountManager
.setProfileAvatar(account
.getProfileKey(), null);
311 public void unregister() throws IOException
{
312 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
313 // If this is the master device, other users can't send messages to this number anymore.
314 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
315 accountManager
.setGcmId(Optional
.absent());
317 account
.setRegistered(false);
321 public List
<DeviceInfo
> getLinkedDevices() throws IOException
{
322 List
<DeviceInfo
> devices
= accountManager
.getDevices();
323 account
.setMultiDevice(devices
.size() > 1);
328 public void removeLinkedDevices(int deviceId
) throws IOException
{
329 accountManager
.removeDevice(deviceId
);
330 List
<DeviceInfo
> devices
= accountManager
.getDevices();
331 account
.setMultiDevice(devices
.size() > 1);
335 public void addDeviceLink(URI linkUri
) throws IOException
, InvalidKeyException
{
336 Utils
.DeviceLinkInfo info
= Utils
.parseDeviceLinkUri(linkUri
);
338 addDevice(info
.deviceIdentifier
, info
.deviceKey
);
341 private void addDevice(String deviceIdentifier
, ECPublicKey deviceKey
) throws IOException
, InvalidKeyException
{
342 IdentityKeyPair identityKeyPair
= getIdentityKeyPair();
343 String verificationCode
= accountManager
.getNewDeviceVerificationCode();
345 accountManager
.addDevice(deviceIdentifier
, deviceKey
, identityKeyPair
, Optional
.of(account
.getProfileKey().serialize()), verificationCode
);
346 account
.setMultiDevice(true);
350 private List
<PreKeyRecord
> generatePreKeys() {
351 List
<PreKeyRecord
> records
= new ArrayList
<>(ServiceConfig
.PREKEY_BATCH_SIZE
);
353 final int offset
= account
.getPreKeyIdOffset();
354 for (int i
= 0; i
< ServiceConfig
.PREKEY_BATCH_SIZE
; i
++) {
355 int preKeyId
= (offset
+ i
) % Medium
.MAX_VALUE
;
356 ECKeyPair keyPair
= Curve
.generateKeyPair();
357 PreKeyRecord
record = new PreKeyRecord(preKeyId
, keyPair
);
362 account
.addPreKeys(records
);
368 private SignedPreKeyRecord
generateSignedPreKey(IdentityKeyPair identityKeyPair
) {
370 ECKeyPair keyPair
= Curve
.generateKeyPair();
371 byte[] signature
= Curve
.calculateSignature(identityKeyPair
.getPrivateKey(), keyPair
.getPublicKey().serialize());
372 SignedPreKeyRecord
record = new SignedPreKeyRecord(account
.getNextSignedPreKeyId(), System
.currentTimeMillis(), keyPair
, signature
);
374 account
.addSignedPreKey(record);
378 } catch (InvalidKeyException e
) {
379 throw new AssertionError(e
);
383 public void verifyAccount(String verificationCode
, String pin
) throws IOException
{
384 verificationCode
= verificationCode
.replace("-", "");
385 account
.setSignalingKey(KeyUtils
.createSignalingKey());
386 // TODO make unrestricted unidentified access configurable
387 VerifyAccountResponse response
= accountManager
.verifyAccountWithCode(verificationCode
, account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, pin
, null, getSelfUnidentifiedAccessKey(), false, ServiceConfig
.capabilities
);
389 UUID uuid
= UuidUtil
.parseOrNull(response
.getUuid());
390 // TODO response.isStorageCapable()
391 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
392 account
.setRegistered(true);
393 account
.setUuid(uuid
);
394 account
.setRegistrationLockPin(pin
);
395 account
.getSignalProtocolStore().saveIdentity(account
.getSelfAddress(), getIdentityKeyPair().getPublicKey(), TrustLevel
.TRUSTED_VERIFIED
);
401 public void setRegistrationLockPin(Optional
<String
> pin
) throws IOException
{
402 if (pin
.isPresent()) {
403 account
.setRegistrationLockPin(pin
.get());
404 throw new RuntimeException("Not implemented anymore, will be replaced with KBS");
406 account
.setRegistrationLockPin(null);
407 accountManager
.removeRegistrationLockV1();
412 void refreshPreKeys() throws IOException
{
413 List
<PreKeyRecord
> oneTimePreKeys
= generatePreKeys();
414 final IdentityKeyPair identityKeyPair
= getIdentityKeyPair();
415 SignedPreKeyRecord signedPreKeyRecord
= generateSignedPreKey(identityKeyPair
);
417 accountManager
.setPreKeys(identityKeyPair
.getPublicKey(), signedPreKeyRecord
, oneTimePreKeys
);
420 private SignalServiceMessageReceiver
getMessageReceiver() {
421 // TODO implement ZkGroup support
422 final ClientZkProfileOperations clientZkProfileOperations
= null;
423 return new SignalServiceMessageReceiver(serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), account
.getSignalingKey(), userAgent
, null, timer
, clientZkProfileOperations
);
426 private SignalServiceMessageSender
getMessageSender() {
427 // TODO implement ZkGroup support
428 final ClientZkProfileOperations clientZkProfileOperations
= null;
429 final boolean attachmentsV3
= false;
430 return new SignalServiceMessageSender(serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(),
431 account
.getDeviceId(), account
.getSignalProtocolStore(), userAgent
, account
.isMultiDevice(), attachmentsV3
, Optional
.fromNullable(messagePipe
), Optional
.fromNullable(unidentifiedMessagePipe
), Optional
.absent(), clientZkProfileOperations
);
434 private SignalServiceProfile
getRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
) throws IOException
{
435 SignalServiceMessagePipe pipe
= unidentifiedMessagePipe
!= null && unidentifiedAccess
.isPresent() ? unidentifiedMessagePipe
440 return pipe
.getProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
441 } catch (IOException ignored
) {
445 SignalServiceMessageReceiver receiver
= getMessageReceiver();
447 return receiver
.retrieveProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
448 } catch (VerificationFailedException e
) {
449 throw new AssertionError(e
);
453 private Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(byte[] groupId
) throws IOException
{
454 File file
= getGroupAvatarFile(groupId
);
455 if (!file
.exists()) {
456 return Optional
.absent();
459 return Optional
.of(Utils
.createAttachment(file
));
462 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(String number
) throws IOException
{
463 File file
= getContactAvatarFile(number
);
464 if (!file
.exists()) {
465 return Optional
.absent();
468 return Optional
.of(Utils
.createAttachment(file
));
471 private GroupInfo
getGroupForSending(byte[] groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
472 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
474 throw new GroupNotFoundException(groupId
);
476 if (!g
.isMember(account
.getSelfAddress())) {
477 throw new NotAGroupMemberException(groupId
, g
.name
);
482 public List
<GroupInfo
> getGroups() {
483 return account
.getGroupStore().getGroups();
487 public long sendGroupMessage(String messageText
, List
<String
> attachments
,
489 throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
490 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
491 if (attachments
!= null) {
492 messageBuilder
.withAttachments(Utils
.getSignalServiceAttachments(attachments
));
494 if (groupId
!= null) {
495 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
498 messageBuilder
.asGroupMessage(group
);
501 final GroupInfo g
= getGroupForSending(groupId
);
503 messageBuilder
.withExpiration(g
.messageExpirationTime
);
505 return sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
508 public void sendGroupMessageReaction(String emoji
, boolean remove
, String targetAuthor
,
509 long targetSentTimestamp
, byte[] groupId
)
510 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
511 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, canonicalizeAndResolveSignalServiceAddress(targetAuthor
), targetSentTimestamp
);
512 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
513 .withReaction(reaction
);
514 if (groupId
!= null) {
515 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
518 messageBuilder
.asGroupMessage(group
);
520 final GroupInfo g
= getGroupForSending(groupId
);
521 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
524 public void sendQuitGroupMessage(byte[] groupId
) throws GroupNotFoundException
, IOException
, EncapsulatedExceptions
{
525 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
529 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
530 .asGroupMessage(group
);
532 final GroupInfo g
= getGroupForSending(groupId
);
533 g
.removeMember(account
.getSelfAddress());
534 account
.getGroupStore().updateGroup(g
);
536 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
539 private byte[] sendUpdateGroupMessage(byte[] groupId
, String name
, Collection
<SignalServiceAddress
> members
, String avatarFile
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
541 if (groupId
== null) {
543 g
= new GroupInfo(KeyUtils
.createGroupId());
544 g
.addMembers(Collections
.singleton(account
.getSelfAddress()));
546 g
= getGroupForSending(groupId
);
553 if (members
!= null) {
554 final Set
<String
> newE164Members
= new HashSet
<>();
555 for (SignalServiceAddress member
: members
) {
556 if (g
.isMember(member
) || !member
.getNumber().isPresent()) {
559 newE164Members
.add(member
.getNumber().get());
562 final List
<ContactTokenDetails
> contacts
= accountManager
.getContacts(newE164Members
);
563 if (contacts
.size() != newE164Members
.size()) {
564 // Some of the new members are not registered on Signal
565 for (ContactTokenDetails contact
: contacts
) {
566 newE164Members
.remove(contact
.getNumber());
568 System
.err
.println("Failed to add members " + Util
.join(", ", newE164Members
) + " to group: Not registered on Signal");
569 System
.err
.println("Aborting…");
573 g
.addMembers(members
);
576 if (avatarFile
!= null) {
577 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
578 File aFile
= getGroupAvatarFile(g
.groupId
);
579 Files
.copy(Paths
.get(avatarFile
), aFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
582 account
.getGroupStore().updateGroup(g
);
584 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
586 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
590 private void sendUpdateGroupMessage(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
591 if (groupId
== null) {
594 GroupInfo g
= getGroupForSending(groupId
);
596 if (!g
.isMember(recipient
)) {
600 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
602 // Send group message only to the recipient who requested it
603 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
606 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfo g
) {
607 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
610 .withMembers(new ArrayList
<>(g
.getMembers()));
612 File aFile
= getGroupAvatarFile(g
.groupId
);
613 if (aFile
.exists()) {
615 group
.withAvatar(Utils
.createAttachment(aFile
));
616 } catch (IOException e
) {
617 throw new AttachmentInvalidException(aFile
.toString(), e
);
621 return SignalServiceDataMessage
.newBuilder()
622 .asGroupMessage(group
.build())
623 .withExpiration(g
.messageExpirationTime
);
626 private void sendGroupInfoRequest(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
627 if (groupId
== null) {
631 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
)
634 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
635 .asGroupMessage(group
.build());
637 // Send group info request message to the recipient who sent us a message with this groupId
638 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
641 private void sendReceipt(SignalServiceAddress remoteAddress
, long messageId
) throws IOException
, UntrustedIdentityException
{
642 SignalServiceReceiptMessage receiptMessage
= new SignalServiceReceiptMessage(SignalServiceReceiptMessage
.Type
.DELIVERY
,
643 Collections
.singletonList(messageId
),
644 System
.currentTimeMillis());
646 getMessageSender().sendReceipt(remoteAddress
, getAccessFor(remoteAddress
), receiptMessage
);
650 public long sendMessage(String message
, List
<String
> attachments
, String recipient
)
651 throws EncapsulatedExceptions
, AttachmentInvalidException
, IOException
, InvalidNumberException
{
652 List
<String
> recipients
= new ArrayList
<>(1);
653 recipients
.add(recipient
);
654 return sendMessage(message
, attachments
, recipients
);
658 public long sendMessage(String messageText
, List
<String
> attachments
,
659 List
<String
> recipients
)
660 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
661 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
662 if (attachments
!= null) {
663 List
<SignalServiceAttachment
> attachmentStreams
= Utils
.getSignalServiceAttachments(attachments
);
665 // Upload attachments here, so we only upload once even for multiple recipients
666 SignalServiceMessageSender messageSender
= getMessageSender();
667 List
<SignalServiceAttachment
> attachmentPointers
= new ArrayList
<>(attachmentStreams
.size());
668 for (SignalServiceAttachment attachment
: attachmentStreams
) {
669 if (attachment
.isStream()) {
670 attachmentPointers
.add(messageSender
.uploadAttachment(attachment
.asStream()));
671 } else if (attachment
.isPointer()) {
672 attachmentPointers
.add(attachment
.asPointer());
676 messageBuilder
.withAttachments(attachmentPointers
);
678 return sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
681 public void sendMessageReaction(String emoji
, boolean remove
, String targetAuthor
,
682 long targetSentTimestamp
, List
<String
> recipients
)
683 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
684 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, canonicalizeAndResolveSignalServiceAddress(targetAuthor
), targetSentTimestamp
);
685 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
686 .withReaction(reaction
);
687 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
691 public void sendEndSessionMessage(List
<String
> recipients
) throws IOException
, EncapsulatedExceptions
, InvalidNumberException
{
692 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
693 .asEndSessionMessage();
695 final Collection
<SignalServiceAddress
> signalServiceAddresses
= getSignalServiceAddresses(recipients
);
697 sendMessageLegacy(messageBuilder
, signalServiceAddresses
);
698 } catch (Exception e
) {
699 for (SignalServiceAddress address
: signalServiceAddresses
) {
700 handleEndSession(address
);
707 public String
getContactName(String number
) throws InvalidNumberException
{
708 ContactInfo contact
= account
.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number
));
709 if (contact
== null) {
717 public void setContactName(String number
, String name
) throws InvalidNumberException
{
718 final SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(number
);
719 ContactInfo contact
= account
.getContactStore().getContact(address
);
720 if (contact
== null) {
721 contact
= new ContactInfo(address
);
722 System
.err
.println("Add contact " + contact
.number
+ " named " + name
);
724 System
.err
.println("Updating contact " + contact
.number
+ " name " + contact
.name
+ " -> " + name
);
727 account
.getContactStore().updateContact(contact
);
732 public void setContactBlocked(String number
, boolean blocked
) throws InvalidNumberException
{
733 setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number
), blocked
);
736 private void setContactBlocked(SignalServiceAddress address
, boolean blocked
) {
737 ContactInfo contact
= account
.getContactStore().getContact(address
);
738 if (contact
== null) {
739 contact
= new ContactInfo(address
);
740 System
.err
.println("Adding and " + (blocked ?
"blocking" : "unblocking") + " contact " + address
.getNumber().orNull());
742 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " contact " + address
.getNumber().orNull());
744 contact
.blocked
= blocked
;
745 account
.getContactStore().updateContact(contact
);
750 public void setGroupBlocked(final byte[] groupId
, final boolean blocked
) throws GroupNotFoundException
{
751 GroupInfo group
= getGroup(groupId
);
753 throw new GroupNotFoundException(groupId
);
755 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " group " + Base64
.encodeBytes(groupId
));
756 group
.blocked
= blocked
;
757 account
.getGroupStore().updateGroup(group
);
763 public List
<byte[]> getGroupIds() {
764 List
<GroupInfo
> groups
= getGroups();
765 List
<byte[]> ids
= new ArrayList
<>(groups
.size());
766 for (GroupInfo group
: groups
) {
767 ids
.add(group
.groupId
);
773 public String
getGroupName(byte[] groupId
) {
774 GroupInfo group
= getGroup(groupId
);
783 public List
<String
> getGroupMembers(byte[] groupId
) {
784 GroupInfo group
= getGroup(groupId
);
786 return Collections
.emptyList();
788 return new ArrayList
<>(group
.getMembersE164());
793 public byte[] updateGroup(byte[] groupId
, String name
, List
<String
> members
, String avatar
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
, InvalidNumberException
{
794 if (groupId
.length
== 0) {
797 if (name
.isEmpty()) {
800 if (members
.size() == 0) {
803 if (avatar
.isEmpty()) {
806 return sendUpdateGroupMessage(groupId
, name
, members
== null ?
null : getSignalServiceAddresses(members
), avatar
);
810 * Change the expiration timer for a contact
812 public void setExpirationTimer(SignalServiceAddress address
, int messageExpirationTimer
) {
813 ContactInfo c
= account
.getContactStore().getContact(address
);
814 c
.messageExpirationTime
= messageExpirationTimer
;
815 account
.getContactStore().updateContact(c
);
819 * Change the expiration timer for a group
821 public void setExpirationTimer(byte[] groupId
, int messageExpirationTimer
) {
822 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
823 g
.messageExpirationTime
= messageExpirationTimer
;
824 account
.getGroupStore().updateGroup(g
);
828 * Upload the sticker pack from path.
830 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
831 * @return if successful, returns the URL to install the sticker pack in the signal app
833 public String
uploadStickerPack(String path
) throws IOException
, StickerPackInvalidException
{
834 SignalServiceStickerManifestUpload manifest
= getSignalServiceStickerManifestUpload(path
);
836 SignalServiceMessageSender messageSender
= getMessageSender();
838 byte[] packKey
= KeyUtils
.createStickerUploadKey();
839 String packId
= messageSender
.uploadStickerManifest(manifest
, packKey
);
842 return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder
.encode(packId
, "utf-8") + "&pack_key=" + URLEncoder
.encode(Hex
.toStringCondensed(packKey
), "utf-8"))
844 } catch (URISyntaxException e
) {
845 throw new AssertionError(e
);
849 private SignalServiceStickerManifestUpload
getSignalServiceStickerManifestUpload(final String path
) throws IOException
, StickerPackInvalidException
{
851 String rootPath
= null;
853 final File file
= new File(path
);
854 if (file
.getName().endsWith(".zip")) {
855 zip
= new ZipFile(file
);
856 } else if (file
.getName().equals("manifest.json")) {
857 rootPath
= file
.getParent();
859 throw new StickerPackInvalidException("Could not find manifest.json");
862 JsonStickerPack pack
= parseStickerPack(rootPath
, zip
);
864 if (pack
.stickers
== null) {
865 throw new StickerPackInvalidException("Must set a 'stickers' field.");
868 if (pack
.stickers
.isEmpty()) {
869 throw new StickerPackInvalidException("Must include stickers.");
872 List
<StickerInfo
> stickers
= new ArrayList
<>(pack
.stickers
.size());
873 for (JsonStickerPack
.JsonSticker sticker
: pack
.stickers
) {
874 if (sticker
.file
== null) {
875 throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
878 Pair
<InputStream
, Long
> data
;
880 data
= getInputStreamAndLength(rootPath
, zip
, sticker
.file
);
881 } catch (IOException ignored
) {
882 throw new StickerPackInvalidException("Could not find find " + sticker
.file
);
885 StickerInfo stickerInfo
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(sticker
.emoji
).or(""));
886 stickers
.add(stickerInfo
);
889 StickerInfo cover
= null;
890 if (pack
.cover
!= null) {
891 if (pack
.cover
.file
== null) {
892 throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
895 Pair
<InputStream
, Long
> data
;
897 data
= getInputStreamAndLength(rootPath
, zip
, pack
.cover
.file
);
898 } catch (IOException ignored
) {
899 throw new StickerPackInvalidException("Could not find find " + pack
.cover
.file
);
902 cover
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(pack
.cover
.emoji
).or(""));
905 return new SignalServiceStickerManifestUpload(
912 private static JsonStickerPack
parseStickerPack(String rootPath
, ZipFile zip
) throws IOException
{
913 InputStream inputStream
;
915 inputStream
= zip
.getInputStream(zip
.getEntry("manifest.json"));
917 inputStream
= new FileInputStream((new File(rootPath
, "manifest.json")));
919 return new ObjectMapper().readValue(inputStream
, JsonStickerPack
.class);
922 private static Pair
<InputStream
, Long
> getInputStreamAndLength(final String rootPath
, final ZipFile zip
, final String subfile
) throws IOException
{
924 final ZipEntry entry
= zip
.getEntry(subfile
);
925 return new Pair
<>(zip
.getInputStream(entry
), entry
.getSize());
927 final File file
= new File(rootPath
, subfile
);
928 return new Pair
<>(new FileInputStream(file
), file
.length());
932 void requestSyncGroups() throws IOException
{
933 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
).build();
934 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
936 sendSyncMessage(message
);
937 } catch (UntrustedIdentityException e
) {
942 void requestSyncContacts() throws IOException
{
943 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
).build();
944 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
946 sendSyncMessage(message
);
947 } catch (UntrustedIdentityException e
) {
952 void requestSyncBlocked() throws IOException
{
953 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
).build();
954 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
956 sendSyncMessage(message
);
957 } catch (UntrustedIdentityException e
) {
962 void requestSyncConfiguration() throws IOException
{
963 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
).build();
964 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
966 sendSyncMessage(message
);
967 } catch (UntrustedIdentityException e
) {
972 private byte[] getSenderCertificate() {
973 // TODO support UUID capable sender certificates
974 // byte[] certificate = accountManager.getSenderCertificate();
977 certificate
= accountManager
.getSenderCertificateLegacy();
978 } catch (IOException e
) {
979 System
.err
.println("Failed to get sender certificate: " + e
);
982 // TODO cache for a day
986 private byte[] getSelfUnidentifiedAccessKey() {
987 return UnidentifiedAccess
.deriveAccessKeyFrom(account
.getProfileKey());
990 private static SignalProfile
decryptProfile(SignalServiceProfile encryptedProfile
, ProfileKey profileKey
) throws IOException
{
991 ProfileCipher profileCipher
= new ProfileCipher(profileKey
);
993 return new SignalProfile(
994 encryptedProfile
.getIdentityKey(),
995 encryptedProfile
.getName() == null ?
null : new String(profileCipher
.decryptName(Base64
.decode(encryptedProfile
.getName()))),
996 encryptedProfile
.getAvatar(),
997 encryptedProfile
.getUnidentifiedAccess() == null || !profileCipher
.verifyUnidentifiedAccess(Base64
.decode(encryptedProfile
.getUnidentifiedAccess())) ?
null : encryptedProfile
.getUnidentifiedAccess(),
998 encryptedProfile
.isUnrestrictedUnidentifiedAccess()
1000 } catch (InvalidCiphertextException e
) {
1005 private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient
) {
1006 ContactInfo contact
= account
.getContactStore().getContact(recipient
);
1007 if (contact
== null || contact
.profileKey
== null) {
1010 ProfileKey theirProfileKey
;
1012 theirProfileKey
= new ProfileKey(Base64
.decode(contact
.profileKey
));
1013 } catch (InvalidInputException
| IOException e
) {
1014 throw new AssertionError(e
);
1016 SignalProfile targetProfile
;
1018 targetProfile
= decryptProfile(getRecipientProfile(recipient
, Optional
.absent()), theirProfileKey
);
1019 } catch (IOException e
) {
1020 System
.err
.println("Failed to get recipient profile: " + e
);
1024 if (targetProfile
== null || targetProfile
.getUnidentifiedAccess() == null) {
1028 if (targetProfile
.isUnrestrictedUnidentifiedAccess()) {
1029 return KeyUtils
.createUnrestrictedUnidentifiedAccess();
1032 return UnidentifiedAccess
.deriveAccessKeyFrom(theirProfileKey
);
1035 private Optional
<UnidentifiedAccessPair
> getAccessForSync() {
1036 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1037 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1039 if (selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1040 return Optional
.absent();
1044 return Optional
.of(new UnidentifiedAccessPair(
1045 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1046 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1048 } catch (InvalidCertificateException e
) {
1049 return Optional
.absent();
1053 private List
<Optional
<UnidentifiedAccessPair
>> getAccessFor(Collection
<SignalServiceAddress
> recipients
) {
1054 List
<Optional
<UnidentifiedAccessPair
>> result
= new ArrayList
<>(recipients
.size());
1055 for (SignalServiceAddress recipient
: recipients
) {
1056 result
.add(getAccessFor(recipient
));
1061 private Optional
<UnidentifiedAccessPair
> getAccessFor(SignalServiceAddress recipient
) {
1062 byte[] recipientUnidentifiedAccessKey
= getTargetUnidentifiedAccessKey(recipient
);
1063 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1064 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1066 if (recipientUnidentifiedAccessKey
== null || selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1067 return Optional
.absent();
1071 return Optional
.of(new UnidentifiedAccessPair(
1072 new UnidentifiedAccess(recipientUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1073 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1075 } catch (InvalidCertificateException e
) {
1076 return Optional
.absent();
1080 private Optional
<UnidentifiedAccess
> getUnidentifiedAccess(SignalServiceAddress recipient
) {
1081 Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1083 if (unidentifiedAccess
.isPresent()) {
1084 return unidentifiedAccess
.get().getTargetUnidentifiedAccess();
1087 return Optional
.absent();
1090 private void sendSyncMessage(SignalServiceSyncMessage message
)
1091 throws IOException
, UntrustedIdentityException
{
1092 SignalServiceMessageSender messageSender
= getMessageSender();
1094 messageSender
.sendMessage(message
, getAccessForSync());
1095 } catch (UntrustedIdentityException e
) {
1096 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1102 * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult.
1104 private long sendMessageLegacy(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1105 throws EncapsulatedExceptions
, IOException
{
1106 final long timestamp
= System
.currentTimeMillis();
1107 messageBuilder
.withTimestamp(timestamp
);
1108 List
<SendMessageResult
> results
= sendMessage(messageBuilder
, recipients
);
1110 List
<UntrustedIdentityException
> untrustedIdentities
= new LinkedList
<>();
1111 List
<UnregisteredUserException
> unregisteredUsers
= new LinkedList
<>();
1112 List
<NetworkFailureException
> networkExceptions
= new LinkedList
<>();
1114 for (SendMessageResult result
: results
) {
1115 if (result
.isUnregisteredFailure()) {
1116 unregisteredUsers
.add(new UnregisteredUserException(result
.getAddress().getLegacyIdentifier(), null));
1117 } else if (result
.isNetworkFailure()) {
1118 networkExceptions
.add(new NetworkFailureException(result
.getAddress().getLegacyIdentifier(), null));
1119 } else if (result
.getIdentityFailure() != null) {
1120 untrustedIdentities
.add(new UntrustedIdentityException("Untrusted", result
.getAddress().getLegacyIdentifier(), result
.getIdentityFailure().getIdentityKey()));
1123 if (!untrustedIdentities
.isEmpty() || !unregisteredUsers
.isEmpty() || !networkExceptions
.isEmpty()) {
1124 throw new EncapsulatedExceptions(untrustedIdentities
, unregisteredUsers
, networkExceptions
);
1129 private Collection
<SignalServiceAddress
> getSignalServiceAddresses(Collection
<String
> numbers
) throws InvalidNumberException
{
1130 final Set
<SignalServiceAddress
> signalServiceAddresses
= new HashSet
<>(numbers
.size());
1132 for (String number
: numbers
) {
1133 signalServiceAddresses
.add(canonicalizeAndResolveSignalServiceAddress(number
));
1135 return signalServiceAddresses
;
1138 private List
<SendMessageResult
> sendMessage(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1139 throws IOException
{
1140 if (messagePipe
== null) {
1141 messagePipe
= getMessageReceiver().createMessagePipe();
1143 if (unidentifiedMessagePipe
== null) {
1144 unidentifiedMessagePipe
= getMessageReceiver().createUnidentifiedMessagePipe();
1146 SignalServiceDataMessage message
= null;
1148 SignalServiceMessageSender messageSender
= getMessageSender();
1150 message
= messageBuilder
.build();
1151 if (message
.getGroupContext().isPresent()) {
1153 final boolean isRecipientUpdate
= false;
1154 List
<SendMessageResult
> result
= messageSender
.sendMessage(new ArrayList
<>(recipients
), getAccessFor(recipients
), isRecipientUpdate
, message
);
1155 for (SendMessageResult r
: result
) {
1156 if (r
.getIdentityFailure() != null) {
1157 account
.getSignalProtocolStore().saveIdentity(r
.getAddress(), r
.getIdentityFailure().getIdentityKey(), TrustLevel
.UNTRUSTED
);
1161 } catch (UntrustedIdentityException e
) {
1162 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1163 return Collections
.emptyList();
1165 } else if (recipients
.size() == 1 && recipients
.contains(account
.getSelfAddress())) {
1166 SignalServiceAddress recipient
= account
.getSelfAddress();
1167 final Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1168 SentTranscriptMessage transcript
= new SentTranscriptMessage(Optional
.of(recipient
),
1169 message
.getTimestamp(),
1171 message
.getExpiresInSeconds(),
1172 Collections
.singletonMap(recipient
, unidentifiedAccess
.isPresent()),
1174 SignalServiceSyncMessage syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
1176 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1178 messageSender
.sendMessage(syncMessage
, unidentifiedAccess
);
1179 } catch (UntrustedIdentityException e
) {
1180 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1181 results
.add(SendMessageResult
.identityFailure(recipient
, e
.getIdentityKey()));
1185 // Send to all individually, so sync messages are sent correctly
1186 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1187 for (SignalServiceAddress address
: recipients
) {
1188 ContactInfo contact
= account
.getContactStore().getContact(address
);
1189 if (contact
!= null) {
1190 messageBuilder
.withExpiration(contact
.messageExpirationTime
);
1191 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
1193 messageBuilder
.withExpiration(0);
1194 messageBuilder
.withProfileKey(null);
1196 message
= messageBuilder
.build();
1198 SendMessageResult result
= messageSender
.sendMessage(address
, getAccessFor(address
), message
);
1199 results
.add(result
);
1200 } catch (UntrustedIdentityException e
) {
1201 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1202 results
.add(SendMessageResult
.identityFailure(address
, e
.getIdentityKey()));
1208 if (message
!= null && message
.isEndSession()) {
1209 for (SignalServiceAddress recipient
: recipients
) {
1210 handleEndSession(recipient
);
1217 private SignalServiceContent
decryptMessage(SignalServiceEnvelope envelope
) throws InvalidMetadataMessageException
, ProtocolInvalidMessageException
, ProtocolDuplicateMessageException
, ProtocolLegacyMessageException
, ProtocolInvalidKeyIdException
, InvalidMetadataVersionException
, ProtocolInvalidVersionException
, ProtocolNoSessionException
, ProtocolInvalidKeyException
, SelfSendException
, UnsupportedDataMessageException
, org
.whispersystems
.libsignal
.UntrustedIdentityException
{
1218 SignalServiceCipher cipher
= new SignalServiceCipher(account
.getSelfAddress(), account
.getSignalProtocolStore(), Utils
.getCertificateValidator());
1220 return cipher
.decrypt(envelope
);
1221 } catch (ProtocolUntrustedIdentityException e
) {
1222 if (e
.getCause() instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
) {
1223 org
.whispersystems
.libsignal
.UntrustedIdentityException identityException
= (org
.whispersystems
.libsignal
.UntrustedIdentityException
) e
.getCause();
1224 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(identityException
.getName()), identityException
.getUntrustedIdentity(), TrustLevel
.UNTRUSTED
);
1225 throw identityException
;
1227 throw new AssertionError(e
);
1231 private void handleEndSession(SignalServiceAddress source
) {
1232 account
.getSignalProtocolStore().deleteAllSessions(source
);
1235 private void handleSignalServiceDataMessage(SignalServiceDataMessage message
, boolean isSync
, SignalServiceAddress source
, SignalServiceAddress destination
, boolean ignoreAttachments
) {
1236 if (message
.getGroupContext().isPresent() && message
.getGroupContext().get().getGroupV1().isPresent()) {
1237 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1238 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1239 switch (groupInfo
.getType()) {
1241 if (group
== null) {
1242 group
= new GroupInfo(groupInfo
.getGroupId());
1245 if (groupInfo
.getAvatar().isPresent()) {
1246 SignalServiceAttachment avatar
= groupInfo
.getAvatar().get();
1247 if (avatar
.isPointer()) {
1249 retrieveGroupAvatarAttachment(avatar
.asPointer(), group
.groupId
);
1250 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1251 System
.err
.println("Failed to retrieve group avatar (" + avatar
.asPointer().getRemoteId() + "): " + e
.getMessage());
1256 if (groupInfo
.getName().isPresent()) {
1257 group
.name
= groupInfo
.getName().get();
1260 if (groupInfo
.getMembers().isPresent()) {
1261 group
.addMembers(groupInfo
.getMembers().get()
1263 .map(this::resolveSignalServiceAddress
)
1264 .collect(Collectors
.toSet()));
1267 account
.getGroupStore().updateGroup(group
);
1270 if (group
== null) {
1272 sendGroupInfoRequest(groupInfo
.getGroupId(), source
);
1273 } catch (IOException
| EncapsulatedExceptions e
) {
1274 e
.printStackTrace();
1279 if (group
!= null) {
1280 group
.removeMember(source
);
1281 account
.getGroupStore().updateGroup(group
);
1285 if (group
!= null) {
1287 sendUpdateGroupMessage(groupInfo
.getGroupId(), source
);
1288 } catch (IOException
| EncapsulatedExceptions e
) {
1289 e
.printStackTrace();
1290 } catch (NotAGroupMemberException e
) {
1291 // We have left this group, so don't send a group update message
1297 final SignalServiceAddress conversationPartnerAddress
= isSync ? destination
: source
;
1298 if (message
.isEndSession()) {
1299 handleEndSession(conversationPartnerAddress
);
1301 if (message
.isExpirationUpdate() || message
.getBody().isPresent()) {
1302 if (message
.getGroupContext().isPresent() && message
.getGroupContext().get().getGroupV1().isPresent()) {
1303 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1304 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1305 if (group
== null) {
1306 group
= new GroupInfo(groupInfo
.getGroupId());
1308 if (group
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1309 group
.messageExpirationTime
= message
.getExpiresInSeconds();
1310 account
.getGroupStore().updateGroup(group
);
1313 ContactInfo contact
= account
.getContactStore().getContact(conversationPartnerAddress
);
1314 if (contact
== null) {
1315 contact
= new ContactInfo(conversationPartnerAddress
);
1317 if (contact
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1318 contact
.messageExpirationTime
= message
.getExpiresInSeconds();
1319 account
.getContactStore().updateContact(contact
);
1323 if (message
.getAttachments().isPresent() && !ignoreAttachments
) {
1324 for (SignalServiceAttachment attachment
: message
.getAttachments().get()) {
1325 if (attachment
.isPointer()) {
1327 retrieveAttachment(attachment
.asPointer());
1328 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1329 System
.err
.println("Failed to retrieve attachment (" + attachment
.asPointer().getRemoteId() + "): " + e
.getMessage());
1334 if (message
.getProfileKey().isPresent() && message
.getProfileKey().get().length
== 32) {
1335 if (source
.matches(account
.getSelfAddress())) {
1337 this.account
.setProfileKey(new ProfileKey(message
.getProfileKey().get()));
1338 } catch (InvalidInputException ignored
) {
1340 ContactInfo contact
= account
.getContactStore().getContact(source
);
1341 if (contact
!= null) {
1342 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1343 account
.getContactStore().updateContact(contact
);
1346 ContactInfo contact
= account
.getContactStore().getContact(source
);
1347 if (contact
== null) {
1348 contact
= new ContactInfo(source
);
1350 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1351 account
.getContactStore().updateContact(contact
);
1354 if (message
.getPreviews().isPresent()) {
1355 final List
<SignalServiceDataMessage
.Preview
> previews
= message
.getPreviews().get();
1356 for (SignalServiceDataMessage
.Preview preview
: previews
) {
1357 if (preview
.getImage().isPresent() && preview
.getImage().get().isPointer()) {
1358 SignalServiceAttachmentPointer attachment
= preview
.getImage().get().asPointer();
1360 retrieveAttachment(attachment
);
1361 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1362 System
.err
.println("Failed to retrieve attachment (" + attachment
.getRemoteId() + "): " + e
.getMessage());
1369 private void retryFailedReceivedMessages(ReceiveMessageHandler handler
, boolean ignoreAttachments
) {
1370 final File cachePath
= new File(getMessageCachePath());
1371 if (!cachePath
.exists()) {
1374 for (final File dir
: Objects
.requireNonNull(cachePath
.listFiles())) {
1375 if (!dir
.isDirectory()) {
1376 retryFailedReceivedMessage(handler
, ignoreAttachments
, dir
);
1380 for (final File fileEntry
: Objects
.requireNonNull(dir
.listFiles())) {
1381 if (!fileEntry
.isFile()) {
1384 retryFailedReceivedMessage(handler
, ignoreAttachments
, fileEntry
);
1386 // Try to delete directory if empty
1391 private void retryFailedReceivedMessage(final ReceiveMessageHandler handler
, final boolean ignoreAttachments
, final File fileEntry
) {
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());
1420 public void receiveMessages(long timeout
, TimeUnit unit
, boolean returnOnTimeout
, boolean ignoreAttachments
, ReceiveMessageHandler handler
) throws IOException
{
1421 retryFailedReceivedMessages(handler
, ignoreAttachments
);
1422 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1425 if (messagePipe
== null) {
1426 messagePipe
= messageReceiver
.createMessagePipe();
1430 SignalServiceEnvelope envelope
;
1431 SignalServiceContent content
= null;
1432 Exception exception
= null;
1433 final long now
= new Date().getTime();
1435 envelope
= messagePipe
.read(timeout
, unit
, envelope1
-> {
1436 // store message on disk, before acknowledging receipt to the server
1438 String source
= envelope1
.getSourceE164().isPresent() ? envelope1
.getSourceE164().get() : "";
1439 File cacheFile
= getMessageCacheFile(source
, now
, envelope1
.getTimestamp());
1440 Utils
.storeEnvelope(envelope1
, cacheFile
);
1441 } catch (IOException e
) {
1442 System
.err
.println("Failed to store encrypted message in disk cache, ignoring: " + e
.getMessage());
1445 } catch (TimeoutException e
) {
1446 if (returnOnTimeout
)
1449 } catch (InvalidVersionException e
) {
1450 System
.err
.println("Ignoring error: " + e
.getMessage());
1453 if (!envelope
.isReceipt()) {
1455 content
= decryptMessage(envelope
);
1456 } catch (Exception e
) {
1459 handleMessage(envelope
, content
, ignoreAttachments
);
1462 if (!isMessageBlocked(envelope
, content
)) {
1463 handler
.handleMessage(envelope
, content
, exception
);
1465 if (!(exception
instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
)) {
1466 File cacheFile
= null;
1468 cacheFile
= getMessageCacheFile(envelope
.getSourceE164().get(), now
, envelope
.getTimestamp());
1469 Files
.delete(cacheFile
.toPath());
1470 // Try to delete directory if empty
1471 new File(getMessageCachePath()).delete();
1472 } catch (IOException e
) {
1473 System
.err
.println("Failed to delete cached message file “" + cacheFile
+ "”: " + e
.getMessage());
1478 if (messagePipe
!= null) {
1479 messagePipe
.shutdown();
1485 private boolean isMessageBlocked(SignalServiceEnvelope envelope
, SignalServiceContent content
) {
1486 SignalServiceAddress source
;
1487 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1488 source
= envelope
.getSourceAddress();
1489 } else if (content
!= null) {
1490 source
= content
.getSender();
1494 ContactInfo sourceContact
= account
.getContactStore().getContact(source
);
1495 if (sourceContact
!= null && sourceContact
.blocked
) {
1499 if (content
!= null && content
.getDataMessage().isPresent()) {
1500 SignalServiceDataMessage message
= content
.getDataMessage().get();
1501 if (message
.getGroupContext().isPresent() && message
.getGroupContext().get().getGroupV1().isPresent()) {
1502 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1503 GroupInfo group
= getGroup(groupInfo
.getGroupId());
1504 if (groupInfo
.getType() == SignalServiceGroup
.Type
.DELIVER
&& group
!= null && group
.blocked
) {
1512 private void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent content
, boolean ignoreAttachments
) {
1513 if (content
!= null) {
1514 SignalServiceAddress sender
;
1515 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1516 sender
= envelope
.getSourceAddress();
1518 sender
= content
.getSender();
1520 if (content
.getDataMessage().isPresent()) {
1521 SignalServiceDataMessage message
= content
.getDataMessage().get();
1523 if (content
.isNeedsReceipt()) {
1525 sendReceipt(sender
, message
.getTimestamp());
1526 } catch (IOException
| UntrustedIdentityException
| IllegalArgumentException e
) {
1527 e
.printStackTrace();
1531 handleSignalServiceDataMessage(message
, false, sender
, account
.getSelfAddress(), ignoreAttachments
);
1533 if (content
.getSyncMessage().isPresent()) {
1534 account
.setMultiDevice(true);
1535 SignalServiceSyncMessage syncMessage
= content
.getSyncMessage().get();
1536 if (syncMessage
.getSent().isPresent()) {
1537 SentTranscriptMessage message
= syncMessage
.getSent().get();
1538 handleSignalServiceDataMessage(message
.getMessage(), true, sender
, message
.getDestination().orNull(), ignoreAttachments
);
1540 if (syncMessage
.getRequest().isPresent()) {
1541 RequestMessage rm
= syncMessage
.getRequest().get();
1542 if (rm
.isContactsRequest()) {
1545 } catch (UntrustedIdentityException
| IOException
| IllegalArgumentException e
) {
1546 e
.printStackTrace();
1549 if (rm
.isGroupsRequest()) {
1552 } catch (UntrustedIdentityException
| IOException
| IllegalArgumentException e
) {
1553 e
.printStackTrace();
1556 if (rm
.isBlockedListRequest()) {
1559 } catch (UntrustedIdentityException
| IOException
| IllegalArgumentException e
) {
1560 e
.printStackTrace();
1563 // TODO Handle rm.isConfigurationRequest();
1565 if (syncMessage
.getGroups().isPresent()) {
1566 File tmpFile
= null;
1568 tmpFile
= IOUtils
.createTempFile();
1569 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(syncMessage
.getGroups().get().asPointer(), tmpFile
)) {
1570 DeviceGroupsInputStream s
= new DeviceGroupsInputStream(attachmentAsStream
);
1572 while ((g
= s
.read()) != null) {
1573 GroupInfo syncGroup
= account
.getGroupStore().getGroup(g
.getId());
1574 if (syncGroup
== null) {
1575 syncGroup
= new GroupInfo(g
.getId());
1577 if (g
.getName().isPresent()) {
1578 syncGroup
.name
= g
.getName().get();
1580 syncGroup
.addMembers(g
.getMembers()
1582 .map(this::resolveSignalServiceAddress
)
1583 .collect(Collectors
.toSet()));
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 setContactBlocked(resolveSignalServiceAddress(address
), true);
1620 for (byte[] groupId
: blockedListMessage
.getGroupIds()) {
1622 setGroupBlocked(groupId
, true);
1623 } catch (GroupNotFoundException e
) {
1624 System
.err
.println("BlockedListMessage contained groupID that was not found in GroupStore: " + Base64
.encodeBytes(groupId
));
1628 if (syncMessage
.getContacts().isPresent()) {
1629 File tmpFile
= null;
1631 tmpFile
= IOUtils
.createTempFile();
1632 final ContactsMessage contactsMessage
= syncMessage
.getContacts().get();
1633 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(contactsMessage
.getContactsStream().asPointer(), tmpFile
)) {
1634 DeviceContactsInputStream s
= new DeviceContactsInputStream(attachmentAsStream
);
1635 if (contactsMessage
.isComplete()) {
1636 account
.getContactStore().clear();
1639 while ((c
= s
.read()) != null) {
1640 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
1641 account
.setProfileKey(c
.getProfileKey().get());
1643 final SignalServiceAddress address
= resolveSignalServiceAddress(c
.getAddress());
1644 ContactInfo contact
= account
.getContactStore().getContact(address
);
1645 if (contact
== null) {
1646 contact
= new ContactInfo(address
);
1648 if (c
.getName().isPresent()) {
1649 contact
.name
= c
.getName().get();
1651 if (c
.getColor().isPresent()) {
1652 contact
.color
= c
.getColor().get();
1654 if (c
.getProfileKey().isPresent()) {
1655 contact
.profileKey
= Base64
.encodeBytes(c
.getProfileKey().get().serialize());
1657 if (c
.getVerified().isPresent()) {
1658 final VerifiedMessage verifiedMessage
= c
.getVerified().get();
1659 account
.getSignalProtocolStore().setIdentityTrustLevel(verifiedMessage
.getDestination(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1661 if (c
.getExpirationTimer().isPresent()) {
1662 contact
.messageExpirationTime
= c
.getExpirationTimer().get();
1664 contact
.blocked
= c
.isBlocked();
1665 contact
.inboxPosition
= c
.getInboxPosition().orNull();
1666 contact
.archived
= c
.isArchived();
1667 account
.getContactStore().updateContact(contact
);
1669 if (c
.getAvatar().isPresent()) {
1670 retrieveContactAvatarAttachment(c
.getAvatar().get(), contact
.number
);
1674 } catch (Exception e
) {
1675 e
.printStackTrace();
1677 if (tmpFile
!= null) {
1679 Files
.delete(tmpFile
.toPath());
1680 } catch (IOException e
) {
1681 System
.err
.println("Failed to delete received contacts temp file “" + tmpFile
+ "”: " + e
.getMessage());
1686 if (syncMessage
.getVerified().isPresent()) {
1687 final VerifiedMessage verifiedMessage
= syncMessage
.getVerified().get();
1688 account
.getSignalProtocolStore().setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage
.getDestination()), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1690 if (syncMessage
.getConfiguration().isPresent()) {
1697 private File
getContactAvatarFile(String number
) {
1698 return new File(pathConfig
.getAvatarsPath(), "contact-" + number
);
1701 private File
retrieveContactAvatarAttachment(SignalServiceAttachment attachment
, String number
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1702 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
1703 if (attachment
.isPointer()) {
1704 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1705 return retrieveAttachment(pointer
, getContactAvatarFile(number
), false);
1707 SignalServiceAttachmentStream stream
= attachment
.asStream();
1708 return Utils
.retrieveAttachment(stream
, getContactAvatarFile(number
));
1712 private File
getGroupAvatarFile(byte[] groupId
) {
1713 return new File(pathConfig
.getAvatarsPath(), "group-" + Base64
.encodeBytes(groupId
).replace("/", "_"));
1716 private File
retrieveGroupAvatarAttachment(SignalServiceAttachment attachment
, byte[] groupId
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1717 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
1718 if (attachment
.isPointer()) {
1719 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1720 return retrieveAttachment(pointer
, getGroupAvatarFile(groupId
), false);
1722 SignalServiceAttachmentStream stream
= attachment
.asStream();
1723 return Utils
.retrieveAttachment(stream
, getGroupAvatarFile(groupId
));
1727 public File
getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId
) {
1728 return new File(pathConfig
.getAttachmentsPath(), attachmentId
.toString());
1731 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1732 IOUtils
.createPrivateDirectories(pathConfig
.getAttachmentsPath());
1733 return retrieveAttachment(pointer
, getAttachmentFile(pointer
.getRemoteId()), true);
1736 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
, File outputFile
, boolean storePreview
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1737 if (storePreview
&& pointer
.getPreview().isPresent()) {
1738 File previewFile
= new File(outputFile
+ ".preview");
1739 try (OutputStream output
= new FileOutputStream(previewFile
)) {
1740 byte[] preview
= pointer
.getPreview().get();
1741 output
.write(preview
, 0, preview
.length
);
1742 } catch (FileNotFoundException e
) {
1743 e
.printStackTrace();
1748 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1750 File tmpFile
= IOUtils
.createTempFile();
1751 try (InputStream input
= messageReceiver
.retrieveAttachment(pointer
, tmpFile
, ServiceConfig
.MAX_ATTACHMENT_SIZE
)) {
1752 try (OutputStream output
= new FileOutputStream(outputFile
)) {
1753 byte[] buffer
= new byte[4096];
1756 while ((read
= input
.read(buffer
)) != -1) {
1757 output
.write(buffer
, 0, read
);
1759 } catch (FileNotFoundException e
) {
1760 e
.printStackTrace();
1765 Files
.delete(tmpFile
.toPath());
1766 } catch (IOException e
) {
1767 System
.err
.println("Failed to delete received attachment temp file “" + tmpFile
+ "”: " + e
.getMessage());
1773 private InputStream
retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer
, File tmpFile
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1774 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1775 return messageReceiver
.retrieveAttachment(pointer
, tmpFile
, ServiceConfig
.MAX_ATTACHMENT_SIZE
);
1779 public boolean isRemote() {
1784 public String
getObjectPath() {
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 JsonIdentityKeyStore
.Identity currentIdentity
= account
.getSignalProtocolStore().getIdentity(record.getAddress());
1831 if (currentIdentity
!= null) {
1832 verifiedMessage
= new VerifiedMessage(record.getAddress(), currentIdentity
.getIdentityKey(), currentIdentity
.getTrustLevel().toVerifiedState(), currentIdentity
.getDateAdded().getTime());
1835 ProfileKey profileKey
= null;
1837 profileKey
= record.profileKey
== null ?
null : new ProfileKey(Base64
.decode(record.profileKey
));
1838 } catch (InvalidInputException ignored
) {
1840 out
.write(new DeviceContact(record.getAddress(), Optional
.fromNullable(record.name
),
1841 createContactAvatarAttachment(record.number
), Optional
.fromNullable(record.color
),
1842 Optional
.fromNullable(verifiedMessage
), Optional
.fromNullable(profileKey
), record.blocked
,
1843 Optional
.of(record.messageExpirationTime
),
1844 Optional
.fromNullable(record.inboxPosition
), record.archived
));
1847 if (account
.getProfileKey() != null) {
1848 // Send our own profile key as well
1849 out
.write(new DeviceContact(account
.getSelfAddress(),
1850 Optional
.absent(), Optional
.absent(),
1851 Optional
.absent(), Optional
.absent(),
1852 Optional
.of(account
.getProfileKey()),
1853 false, Optional
.absent(), Optional
.absent(), false));
1857 if (contactsFile
.exists() && contactsFile
.length() > 0) {
1858 try (FileInputStream contactsFileStream
= new FileInputStream(contactsFile
)) {
1859 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1860 .withStream(contactsFileStream
)
1861 .withContentType("application/octet-stream")
1862 .withLength(contactsFile
.length())
1865 sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
, true)));
1870 Files
.delete(contactsFile
.toPath());
1871 } catch (IOException e
) {
1872 System
.err
.println("Failed to delete contacts temp file “" + contactsFile
+ "”: " + e
.getMessage());
1877 private void sendBlockedList() throws IOException
, UntrustedIdentityException
{
1878 List
<SignalServiceAddress
> addresses
= new ArrayList
<>();
1879 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1880 if (record.blocked
) {
1881 addresses
.add(record.getAddress());
1884 List
<byte[]> groupIds
= new ArrayList
<>();
1885 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1886 if (record.blocked
) {
1887 groupIds
.add(record.groupId
);
1890 sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
1893 private void sendVerifiedMessage(SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
) throws IOException
, UntrustedIdentityException
{
1894 VerifiedMessage verifiedMessage
= new VerifiedMessage(destination
, identityKey
, trustLevel
.toVerifiedState(), System
.currentTimeMillis());
1895 sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
1898 public List
<ContactInfo
> getContacts() {
1899 return account
.getContactStore().getContacts();
1902 public ContactInfo
getContact(String number
) {
1903 return account
.getContactStore().getContact(Util
.getSignalServiceAddressFromIdentifier(number
));
1906 public GroupInfo
getGroup(byte[] groupId
) {
1907 return account
.getGroupStore().getGroup(groupId
);
1910 public List
<JsonIdentityKeyStore
.Identity
> getIdentities() {
1911 return account
.getSignalProtocolStore().getIdentities();
1914 public List
<JsonIdentityKeyStore
.Identity
> getIdentities(String number
) throws InvalidNumberException
{
1915 return account
.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number
));
1919 * Trust this the identity with this fingerprint
1921 * @param name username of the identity
1922 * @param fingerprint Fingerprint
1924 public boolean trustIdentityVerified(String name
, byte[] fingerprint
) throws InvalidNumberException
{
1925 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
1926 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
1930 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1931 if (!Arrays
.equals(id
.getIdentityKey().serialize(), fingerprint
)) {
1935 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1937 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1938 } catch (IOException
| UntrustedIdentityException e
) {
1939 e
.printStackTrace();
1948 * Trust this the identity with this safety number
1950 * @param name username of the identity
1951 * @param safetyNumber Safety number
1953 public boolean trustIdentityVerifiedSafetyNumber(String name
, String safetyNumber
) throws InvalidNumberException
{
1954 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
1955 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
1959 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1960 if (!safetyNumber
.equals(computeSafetyNumber(address
, id
.getIdentityKey()))) {
1964 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1966 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1967 } catch (IOException
| UntrustedIdentityException e
) {
1968 e
.printStackTrace();
1977 * Trust all keys of this identity without verification
1979 * @param name username of the identity
1981 public boolean trustIdentityAllKeys(String name
) {
1982 SignalServiceAddress address
= resolveSignalServiceAddress(name
);
1983 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
1987 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1988 if (id
.getTrustLevel() == TrustLevel
.UNTRUSTED
) {
1989 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
1991 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
1992 } catch (IOException
| UntrustedIdentityException e
) {
1993 e
.printStackTrace();
2001 public String
computeSafetyNumber(SignalServiceAddress theirAddress
, IdentityKey theirIdentityKey
) {
2002 return Utils
.computeSafetyNumber(account
.getSelfAddress(), getIdentityKeyPair().getPublicKey(), theirAddress
, theirIdentityKey
);
2005 void saveAccount() {
2009 public SignalServiceAddress
canonicalizeAndResolveSignalServiceAddress(String identifier
) throws InvalidNumberException
{
2010 String canonicalizedNumber
= UuidUtil
.isUuid(identifier
) ? identifier
: Util
.canonicalizeNumber(identifier
, account
.getUsername());
2011 return resolveSignalServiceAddress(canonicalizedNumber
);
2014 public SignalServiceAddress
resolveSignalServiceAddress(String identifier
) {
2015 SignalServiceAddress address
= Util
.getSignalServiceAddressFromIdentifier(identifier
);
2017 return resolveSignalServiceAddress(address
);
2020 public SignalServiceAddress
resolveSignalServiceAddress(SignalServiceAddress address
) {
2021 if (address
.matches(account
.getSelfAddress())) {
2022 return account
.getSelfAddress();
2025 return account
.getRecipientStore().resolveServiceAddress(address
);
2028 public interface ReceiveMessageHandler
{
2030 void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent decryptedContent
, Throwable e
);