Merge branch 'AsamK:master' into master

This commit is contained in:
Adimarantis 2021-09-05 18:45:07 +02:00 committed by GitHub
commit f2c7c60669
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1146 additions and 86 deletions

View file

@ -10,6 +10,7 @@
### Added ### Added
- New global parameter `--trust-new-identities=always` to allow trusting any new identity key without verification - New global parameter `--trust-new-identities=always` to allow trusting any new identity key without verification
- New parameter `--device-name` for `updateAccount` command to update the device name
## [0.8.5] - 2021-08-07 ## [0.8.5] - 2021-08-07
### Added ### Added

View file

@ -2272,7 +2272,6 @@
"fields":[ "fields":[
{"name":"bitField0_"}, {"name":"bitField0_"},
{"name":"e164_"}, {"name":"e164_"},
{"name":"relay_"},
{"name":"uuid_"} {"name":"uuid_"}
] ]
}, },

View file

@ -14,7 +14,7 @@ repositories {
} }
dependencies { dependencies {
api("com.github.turasa:signal-service-java:2.15.3_unofficial_26") api("com.github.turasa:signal-service-java:2.15.3_unofficial_27")
implementation("com.google.protobuf:protobuf-javalite:3.10.0") implementation("com.google.protobuf:protobuf-javalite:3.10.0")
implementation("org.bouncycastle:bcprov-jdk15on:1.69") implementation("org.bouncycastle:bcprov-jdk15on:1.69")
implementation("org.slf4j:slf4j-api:1.7.30") implementation("org.slf4j:slf4j-api:1.7.30")

View file

@ -42,6 +42,7 @@ import org.asamk.signal.manager.helper.IncomingMessageHandler;
import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.PinHelper;
import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.ProfileHelper;
import org.asamk.signal.manager.helper.SendHelper; import org.asamk.signal.manager.helper.SendHelper;
import org.asamk.signal.manager.helper.StorageHelper;
import org.asamk.signal.manager.helper.SyncHelper; import org.asamk.signal.manager.helper.SyncHelper;
import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; import org.asamk.signal.manager.helper.UnidentifiedAccessHelper;
import org.asamk.signal.manager.jobs.Context; import org.asamk.signal.manager.jobs.Context;
@ -133,6 +134,7 @@ public class Manager implements Closeable {
private final ProfileHelper profileHelper; private final ProfileHelper profileHelper;
private final PinHelper pinHelper; private final PinHelper pinHelper;
private final StorageHelper storageHelper;
private final SendHelper sendHelper; private final SendHelper sendHelper;
private final SyncHelper syncHelper; private final SyncHelper syncHelper;
private final AttachmentHelper attachmentHelper; private final AttachmentHelper attachmentHelper;
@ -141,6 +143,7 @@ public class Manager implements Closeable {
private final IncomingMessageHandler incomingMessageHandler; private final IncomingMessageHandler incomingMessageHandler;
private final Context context; private final Context context;
private boolean hasCaughtUpWithOldMessages = false;
Manager( Manager(
SignalAccount account, SignalAccount account,
@ -208,6 +211,7 @@ public class Manager implements Closeable {
avatarStore, avatarStore,
this::resolveSignalServiceAddress, this::resolveSignalServiceAddress,
account.getRecipientStore()); account.getRecipientStore());
this.storageHelper = new StorageHelper(account, dependencies, groupHelper);
this.contactHelper = new ContactHelper(account); this.contactHelper = new ContactHelper(account);
this.syncHelper = new SyncHelper(account, this.syncHelper = new SyncHelper(account,
attachmentHelper, attachmentHelper,
@ -222,7 +226,8 @@ public class Manager implements Closeable {
sendHelper, sendHelper,
groupHelper, groupHelper,
syncHelper, syncHelper,
profileHelper); profileHelper,
storageHelper);
var jobExecutor = new JobExecutor(context); var jobExecutor = new JobExecutor(context);
this.incomingMessageHandler = new IncomingMessageHandler(account, this.incomingMessageHandler = new IncomingMessageHandler(account,
@ -310,7 +315,7 @@ public class Manager implements Closeable {
if (account.getUuid() == null) { if (account.getUuid() == null) {
account.setUuid(dependencies.getAccountManager().getOwnUuid()); account.setUuid(dependencies.getAccountManager().getOwnUuid());
} }
updateAccountAttributes(); updateAccountAttributes(null);
} }
/** /**
@ -342,14 +347,21 @@ public class Manager implements Closeable {
})); }));
} }
public void updateAccountAttributes() throws IOException { public void updateAccountAttributes(String deviceName) throws IOException {
final String encryptedDeviceName;
if (deviceName == null) {
encryptedDeviceName = account.getEncryptedDeviceName();
} else {
final var privateKey = account.getIdentityKeyPair().getPrivateKey();
encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
account.setEncryptedDeviceName(encryptedDeviceName);
}
dependencies.getAccountManager() dependencies.getAccountManager()
.setAccountAttributes(account.getEncryptedDeviceName(), .setAccountAttributes(encryptedDeviceName,
null, null,
account.getLocalRegistrationId(), account.getLocalRegistrationId(),
true, true,
// set legacy pin only if no KBS master key is set null,
account.getPinMasterKey() == null ? account.getRegistrationLockPin() : null,
account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(), account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(),
account.getSelfUnidentifiedAccessKey(), account.getSelfUnidentifiedAccessKey(),
account.isUnrestrictedUnidentifiedAccess(), account.isUnrestrictedUnidentifiedAccess(),
@ -739,6 +751,13 @@ public class Manager implements Closeable {
public void requestAllSyncData() throws IOException { public void requestAllSyncData() throws IOException {
syncHelper.requestAllSyncData(); syncHelper.requestAllSyncData();
retrieveRemoteStorage();
}
void retrieveRemoteStorage() throws IOException {
if (account.getStorageKey() != null) {
storageHelper.readDataFromStorage();
}
} }
private byte[] getSenderCertificate() { private byte[] getSenderCertificate() {
@ -865,7 +884,7 @@ public class Manager implements Closeable {
final var signalWebSocket = dependencies.getSignalWebSocket(); final var signalWebSocket = dependencies.getSignalWebSocket();
signalWebSocket.connect(); signalWebSocket.connect();
var hasCaughtUpWithOldMessages = false; hasCaughtUpWithOldMessages = false;
while (!Thread.interrupted()) { while (!Thread.interrupted()) {
SignalServiceEnvelope envelope; SignalServiceEnvelope envelope;
@ -885,11 +904,14 @@ public class Manager implements Closeable {
envelope = result.get(); envelope = result.get();
} else { } else {
// Received indicator that server queue is empty // Received indicator that server queue is empty
hasCaughtUpWithOldMessages = true;
handleQueuedActions(queuedActions); handleQueuedActions(queuedActions);
queuedActions.clear(); queuedActions.clear();
hasCaughtUpWithOldMessages = true;
synchronized (this) {
this.notifyAll();
}
// Continue to wait another timeout for new messages // Continue to wait another timeout for new messages
continue; continue;
} }
@ -936,17 +958,27 @@ public class Manager implements Closeable {
handleQueuedActions(queuedActions); handleQueuedActions(queuedActions);
} }
public boolean hasCaughtUpWithOldMessages() {
return hasCaughtUpWithOldMessages;
}
private void handleQueuedActions(final Collection<HandleAction> queuedActions) { private void handleQueuedActions(final Collection<HandleAction> queuedActions) {
var interrupted = false;
for (var action : queuedActions) { for (var action : queuedActions) {
try { try {
action.execute(context); action.execute(context);
} catch (Throwable e) { } catch (Throwable e) {
if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { if ((e instanceof AssertionError || e instanceof RuntimeException)
Thread.currentThread().interrupt(); && e.getCause() instanceof InterruptedException) {
interrupted = true;
continue;
} }
logger.warn("Message action failed.", e); logger.warn("Message action failed.", e);
} }
} }
if (interrupted) {
Thread.currentThread().interrupt();
}
} }
public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {

View file

@ -103,6 +103,7 @@ public class ProvisioningManager {
? null ? null
: DeviceNameUtil.encryptDeviceName(deviceName, ret.getIdentity().getPrivateKey()); : DeviceNameUtil.encryptDeviceName(deviceName, ret.getIdentity().getPrivateKey());
logger.debug("Finishing new device registration");
var deviceId = accountManager.finishNewDeviceRegistration(ret.getProvisioningCode(), var deviceId = accountManager.finishNewDeviceRegistration(ret.getProvisioningCode(),
false, false,
true, true,
@ -129,6 +130,7 @@ public class ProvisioningManager {
try { try {
m = new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent); m = new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent);
logger.debug("Refreshing pre keys");
try { try {
m.refreshPreKeys(); m.refreshPreKeys();
} catch (Exception e) { } catch (Exception e) {
@ -136,6 +138,7 @@ public class ProvisioningManager {
throw e; throw e;
} }
logger.debug("Requesting sync data");
try { try {
m.requestAllSyncData(); m.requestAllSyncData();
} catch (Exception e) { } catch (Exception e) {

View file

@ -35,7 +35,9 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.ServiceResponse;
import org.whispersystems.signalservice.internal.push.LockedException; import org.whispersystems.signalservice.internal.push.LockedException;
import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
@ -115,13 +117,19 @@ public class RegistrationManager implements Closeable {
} }
public void register(boolean voiceVerification, String captcha) throws IOException { public void register(boolean voiceVerification, String captcha) throws IOException {
final ServiceResponse<RequestVerificationCodeResponse> response;
if (voiceVerification) { if (voiceVerification) {
accountManager.requestVoiceVerificationCode(getDefaultLocale(), response = accountManager.requestVoiceVerificationCode(getDefaultLocale(),
Optional.fromNullable(captcha), Optional.fromNullable(captcha),
Optional.absent(),
Optional.absent()); Optional.absent());
} else { } else {
accountManager.requestSmsVerificationCode(false, Optional.fromNullable(captcha), Optional.absent()); response = accountManager.requestSmsVerificationCode(false,
Optional.fromNullable(captcha),
Optional.absent(),
Optional.absent());
} }
handleResponseException(response);
} }
private Locale getDefaultLocale() { private Locale getDefaultLocale() {
@ -143,7 +151,7 @@ public class RegistrationManager implements Closeable {
VerifyAccountResponse response; VerifyAccountResponse response;
MasterKey masterKey; MasterKey masterKey;
try { try {
response = verifyAccountWithCode(verificationCode, null, null); response = verifyAccountWithCode(verificationCode, null);
masterKey = null; masterKey = null;
pin = null; pin = null;
@ -154,20 +162,18 @@ public class RegistrationManager implements Closeable {
var registrationLockData = pinHelper.getRegistrationLockData(pin, e); var registrationLockData = pinHelper.getRegistrationLockData(pin, e);
if (registrationLockData == null) { if (registrationLockData == null) {
response = verifyAccountWithCode(verificationCode, pin, null); throw e;
masterKey = null;
} else {
var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock();
try {
response = verifyAccountWithCode(verificationCode, null, registrationLock);
} catch (LockedException _e) {
throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
}
masterKey = registrationLockData.getMasterKey();
} }
var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock();
try {
response = verifyAccountWithCode(verificationCode, registrationLock);
} catch (LockedException _e) {
throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
}
masterKey = registrationLockData.getMasterKey();
} }
// TODO response.isStorageCapable()
//accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
account.finishRegistration(UuidUtil.parseOrNull(response.getUuid()), masterKey, pin); account.finishRegistration(UuidUtil.parseOrNull(response.getUuid()), masterKey, pin);
@ -179,6 +185,9 @@ public class RegistrationManager implements Closeable {
m.refreshPreKeys(); m.refreshPreKeys();
// Set an initial empty profile so user can be added to groups // Set an initial empty profile so user can be added to groups
m.setProfile(null, null, null, null, null); m.setProfile(null, null, null, null, null);
if (response.isStorageCapable()) {
m.retrieveRemoteStorage();
}
final var result = m; final var result = m;
m = null; m = null;
@ -192,18 +201,29 @@ public class RegistrationManager implements Closeable {
} }
private VerifyAccountResponse verifyAccountWithCode( private VerifyAccountResponse verifyAccountWithCode(
final String verificationCode, final String legacyPin, final String registrationLock final String verificationCode, final String registrationLock
) throws IOException { ) throws IOException {
return accountManager.verifyAccountWithCode(verificationCode, final ServiceResponse<VerifyAccountResponse> response;
null, if (registrationLock == null) {
account.getLocalRegistrationId(), response = accountManager.verifyAccount(verificationCode,
true, account.getLocalRegistrationId(),
legacyPin, true,
registrationLock, account.getSelfUnidentifiedAccessKey(),
account.getSelfUnidentifiedAccessKey(), account.isUnrestrictedUnidentifiedAccess(),
account.isUnrestrictedUnidentifiedAccess(), ServiceConfig.capabilities,
ServiceConfig.capabilities, account.isDiscoverableByPhoneNumber());
account.isDiscoverableByPhoneNumber()); } else {
response = accountManager.verifyAccountWithRegistrationLockPin(verificationCode,
account.getLocalRegistrationId(),
true,
registrationLock,
account.getSelfUnidentifiedAccessKey(),
account.isUnrestrictedUnidentifiedAccess(),
ServiceConfig.capabilities,
account.isDiscoverableByPhoneNumber());
}
handleResponseException(response);
return response.getResult().get();
} }
@Override @Override
@ -213,4 +233,15 @@ public class RegistrationManager implements Closeable {
account = null; account = null;
} }
} }
private void handleResponseException(final ServiceResponse<?> response) throws IOException {
final var throwableOptional = response.getExecutionError().or(response.getApplicationError());
if (throwableOptional.isPresent()) {
if (throwableOptional.get() instanceof IOException) {
throw (IOException) throwableOptional.get();
} else {
throw new IOException(throwableOptional.get());
}
}
}
} }

View file

@ -1,6 +1,7 @@
package org.asamk.signal.manager; package org.asamk.signal.manager;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
public enum TrustLevel { public enum TrustLevel {
UNTRUSTED, UNTRUSTED,
@ -16,6 +17,20 @@ public enum TrustLevel {
return TrustLevel.cachedValues[i]; return TrustLevel.cachedValues[i];
} }
public static TrustLevel fromIdentityState(ContactRecord.IdentityState identityState) {
switch (identityState) {
case DEFAULT:
return TRUSTED_UNVERIFIED;
case UNVERIFIED:
return UNTRUSTED;
case VERIFIED:
return TRUSTED_VERIFIED;
case UNRECOGNIZED:
return null;
}
throw new RuntimeException("Unknown identity state: " + identityState);
}
public static TrustLevel fromVerifiedState(VerifiedMessage.VerifiedState verifiedState) { public static TrustLevel fromVerifiedState(VerifiedMessage.VerifiedState verifiedState) {
switch (verifiedState) { switch (verifiedState) {
case DEFAULT: case DEFAULT:

View file

@ -0,0 +1,26 @@
package org.asamk.signal.manager.actions;
import org.asamk.signal.manager.jobs.Context;
public class RetrieveStorageDataAction implements HandleAction {
private static final RetrieveStorageDataAction INSTANCE = new RetrieveStorageDataAction();
private RetrieveStorageDataAction() {
}
public static RetrieveStorageDataAction create() {
return INSTANCE;
}
@Override
public void execute(Context context) throws Throwable {
if (context.getAccount().getStorageKey() != null) {
context.getStorageHelper().readDataFromStorage();
} else {
if (!context.getAccount().isMasterDevice()) {
context.getSyncHelper().requestAllSyncData();
}
}
}
}

View file

@ -0,0 +1,20 @@
package org.asamk.signal.manager.actions;
import org.asamk.signal.manager.jobs.Context;
public class SendSyncKeysAction implements HandleAction {
private static final SendSyncKeysAction INSTANCE = new SendSyncKeysAction();
private SendSyncKeysAction() {
}
public static SendSyncKeysAction create() {
return INSTANCE;
}
@Override
public void execute(Context context) throws Throwable {
context.getSyncHelper().sendKeysMessage();
}
}

View file

@ -29,7 +29,7 @@ class SandboxConfig {
private final static String KEY_BACKUP_ENCLAVE_NAME = "823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9"; private final static String KEY_BACKUP_ENCLAVE_NAME = "823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9";
private final static byte[] KEY_BACKUP_SERVICE_ID = Hex.decode( private final static byte[] KEY_BACKUP_SERVICE_ID = Hex.decode(
"51a56084c0b21c6b8f62b1bc792ec9bedac4c7c3964bb08ddcab868158c09982"); "16b94ac6d2b7f7b9d72928f36d798dbb35ed32e7bb14c42b4301ad0344b46f29");
private final static String KEY_BACKUP_MRENCLAVE = "a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87"; private final static String KEY_BACKUP_MRENCLAVE = "a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87";
private final static String URL = "https://chat.staging.signal.org"; private final static String URL = "https://chat.staging.signal.org";

View file

@ -34,12 +34,7 @@ public class ServiceConfig {
} catch (Throwable ignored) { } catch (Throwable ignored) {
zkGroupAvailable = false; zkGroupAvailable = false;
} }
capabilities = new AccountAttributes.Capabilities(false, capabilities = new AccountAttributes.Capabilities(false, zkGroupAvailable, false, zkGroupAvailable, true, true);
zkGroupAvailable,
false,
zkGroupAvailable,
false,
true);
try { try {
TrustStore contactTrustStore = new IasTrustStore(); TrustStore contactTrustStore = new IasTrustStore();

View file

@ -8,12 +8,14 @@ import org.asamk.signal.manager.UntrustedIdentityException;
import org.asamk.signal.manager.actions.HandleAction; import org.asamk.signal.manager.actions.HandleAction;
import org.asamk.signal.manager.actions.RenewSessionAction; import org.asamk.signal.manager.actions.RenewSessionAction;
import org.asamk.signal.manager.actions.RetrieveProfileAction; import org.asamk.signal.manager.actions.RetrieveProfileAction;
import org.asamk.signal.manager.actions.RetrieveStorageDataAction;
import org.asamk.signal.manager.actions.SendGroupInfoAction; import org.asamk.signal.manager.actions.SendGroupInfoAction;
import org.asamk.signal.manager.actions.SendGroupInfoRequestAction; import org.asamk.signal.manager.actions.SendGroupInfoRequestAction;
import org.asamk.signal.manager.actions.SendReceiptAction; import org.asamk.signal.manager.actions.SendReceiptAction;
import org.asamk.signal.manager.actions.SendSyncBlockedListAction; import org.asamk.signal.manager.actions.SendSyncBlockedListAction;
import org.asamk.signal.manager.actions.SendSyncContactsAction; import org.asamk.signal.manager.actions.SendSyncContactsAction;
import org.asamk.signal.manager.actions.SendSyncGroupsAction; import org.asamk.signal.manager.actions.SendSyncGroupsAction;
import org.asamk.signal.manager.actions.SendSyncKeysAction;
import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.GroupUtils;
@ -30,6 +32,7 @@ import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
@ -173,10 +176,20 @@ public final class IncomingMessageHandler {
) { ) {
var actions = new ArrayList<HandleAction>(); var actions = new ArrayList<HandleAction>();
final RecipientId sender; final RecipientId sender;
final int senderDeviceId;
if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) { if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) {
sender = recipientResolver.resolveRecipient(envelope.getSourceAddress()); sender = recipientResolver.resolveRecipient(envelope.getSourceAddress());
senderDeviceId = envelope.getSourceDevice();
} else { } else {
sender = recipientResolver.resolveRecipient(content.getSender()); sender = recipientResolver.resolveRecipient(content.getSender());
senderDeviceId = content.getSenderDevice();
}
if (content.getSenderKeyDistributionMessage().isPresent()) {
final var message = content.getSenderKeyDistributionMessage().get();
final var protocolAddress = new SignalProtocolAddress(addressResolver.resolveSignalServiceAddress(sender)
.getIdentifier(), senderDeviceId);
dependencies.getMessageSender().processSenderKeyDistributionMessage(protocolAddress, message);
} }
if (content.getDataMessage().isPresent()) { if (content.getDataMessage().isPresent()) {
@ -226,7 +239,10 @@ public final class IncomingMessageHandler {
if (rm.isBlockedListRequest()) { if (rm.isBlockedListRequest()) {
actions.add(SendSyncBlockedListAction.create()); actions.add(SendSyncBlockedListAction.create());
} }
// TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest(); if (rm.isKeysRequest()) {
actions.add(SendSyncKeysAction.create());
}
// TODO Handle rm.isConfigurationRequest();
} }
if (syncMessage.getGroups().isPresent()) { if (syncMessage.getGroups().isPresent()) {
logger.warn("Received a group v1 sync message, that can't be handled anymore, ignoring."); logger.warn("Received a group v1 sync message, that can't be handled anymore, ignoring.");
@ -296,7 +312,7 @@ public final class IncomingMessageHandler {
case LOCAL_PROFILE: case LOCAL_PROFILE:
actions.add(new RetrieveProfileAction(account.getSelfRecipientId())); actions.add(new RetrieveProfileAction(account.getSelfRecipientId()));
case STORAGE_MANIFEST: case STORAGE_MANIFEST:
// TODO actions.add(RetrieveStorageDataAction.create());
} }
} }
if (syncMessage.getKeys().isPresent()) { if (syncMessage.getKeys().isPresent()) {
@ -304,6 +320,7 @@ public final class IncomingMessageHandler {
if (keysMessage.getStorageService().isPresent()) { if (keysMessage.getStorageService().isPresent()) {
final var storageKey = keysMessage.getStorageService().get(); final var storageKey = keysMessage.getStorageService().get();
account.setStorageKey(storageKey); account.setStorageKey(storageKey);
actions.add(RetrieveStorageDataAction.create());
} }
} }
if (syncMessage.getConfiguration().isPresent()) { if (syncMessage.getConfiguration().isPresent()) {

View file

@ -0,0 +1,9 @@
package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.asamk.signal.manager.storage.recipients.RecipientId;
public interface RecipientAddressResolver {
RecipientAddress resolveRecipientAddress(RecipientId recipientId);
}

View file

@ -44,20 +44,6 @@ public class SendHelper {
private final GroupProvider groupProvider; private final GroupProvider groupProvider;
private final RecipientRegistrationRefresher recipientRegistrationRefresher; private final RecipientRegistrationRefresher recipientRegistrationRefresher;
private final SignalServiceMessageSender.IndividualSendEvents sendEvents = new SignalServiceMessageSender.IndividualSendEvents() {
@Override
public void onMessageEncrypted() {
}
@Override
public void onMessageSent() {
}
@Override
public void onSyncMessageSent() {
}
};
public SendHelper( public SendHelper(
final SignalAccount account, final SignalAccount account,
final SignalDependencies dependencies, final SignalDependencies dependencies,
@ -267,6 +253,7 @@ public class SendHelper {
isRecipientUpdate, isRecipientUpdate,
ContentHint.DEFAULT, ContentHint.DEFAULT,
message, message,
SignalServiceMessageSender.LegacyGroupEvents.EMPTY,
sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()), sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()),
() -> false); () -> false);
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
@ -286,14 +273,14 @@ public class SendHelper {
unidentifiedAccessHelper.getAccessFor(recipientId), unidentifiedAccessHelper.getAccessFor(recipientId),
ContentHint.DEFAULT, ContentHint.DEFAULT,
message, message,
sendEvents); SignalServiceMessageSender.IndividualSendEvents.EMPTY);
} catch (UnregisteredUserException e) { } catch (UnregisteredUserException e) {
final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId); final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId);
return messageSender.sendDataMessage(addressResolver.resolveSignalServiceAddress(newRecipientId), return messageSender.sendDataMessage(addressResolver.resolveSignalServiceAddress(newRecipientId),
unidentifiedAccessHelper.getAccessFor(newRecipientId), unidentifiedAccessHelper.getAccessFor(newRecipientId),
ContentHint.DEFAULT, ContentHint.DEFAULT,
message, message,
sendEvents); SignalServiceMessageSender.IndividualSendEvents.EMPTY);
} }
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
return SendMessageResult.identityFailure(address, e.getIdentityKey()); return SendMessageResult.identityFailure(address, e.getIdentityKey());

View file

@ -0,0 +1,222 @@
package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.SignalDependencies;
import org.asamk.signal.manager.TrustLevel;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.recipients.Contact;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.signal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
public class StorageHelper {
private final static Logger logger = LoggerFactory.getLogger(StorageHelper.class);
private final SignalAccount account;
private final SignalDependencies dependencies;
private final GroupHelper groupHelper;
public StorageHelper(
final SignalAccount account, final SignalDependencies dependencies, final GroupHelper groupHelper
) {
this.account = account;
this.dependencies = dependencies;
this.groupHelper = groupHelper;
}
public void readDataFromStorage() throws IOException {
logger.debug("Reading data from remote storage");
Optional<SignalStorageManifest> manifest;
try {
manifest = dependencies.getAccountManager()
.getStorageManifestIfDifferentVersion(account.getStorageKey(), account.getStorageManifestVersion());
} catch (InvalidKeyException e) {
logger.warn("Manifest couldn't be decrypted, ignoring.");
return;
}
if (!manifest.isPresent()) {
logger.debug("Manifest is up to date, does not exist or couldn't be decrypted, ignoring.");
return;
}
account.setStorageManifestVersion(manifest.get().getVersion());
readAccountRecord(manifest.get());
final var storageIds = manifest.get()
.getStorageIds()
.stream()
.filter(id -> !id.isUnknown() && id.getType() != ManifestRecord.Identifier.Type.ACCOUNT_VALUE)
.collect(Collectors.toList());
for (final var record : getSignalStorageRecords(storageIds)) {
if (record.getType() == ManifestRecord.Identifier.Type.GROUPV2_VALUE) {
readGroupV2Record(record);
} else if (record.getType() == ManifestRecord.Identifier.Type.GROUPV1_VALUE) {
readGroupV1Record(record);
} else if (record.getType() == ManifestRecord.Identifier.Type.CONTACT_VALUE) {
readContactRecord(record);
}
}
}
private void readContactRecord(final SignalStorageRecord record) {
if (record == null || !record.getContact().isPresent()) {
return;
}
final var contactRecord = record.getContact().get();
final var address = contactRecord.getAddress();
final var recipientId = account.getRecipientStore().resolveRecipient(address);
final var contact = account.getContactStore().getContact(recipientId);
if (contactRecord.getGivenName().isPresent() || contactRecord.getFamilyName().isPresent() || (
(contact == null || !contact.isBlocked()) && contactRecord.isBlocked()
)) {
final var newContact = (contact == null ? Contact.newBuilder() : Contact.newBuilder(contact)).withBlocked(
contactRecord.isBlocked())
.withName((contactRecord.getGivenName().or("") + " " + contactRecord.getFamilyName().or("")).trim())
.build();
account.getContactStore().storeContact(recipientId, newContact);
}
if (contactRecord.getProfileKey().isPresent()) {
try {
final var profileKey = new ProfileKey(contactRecord.getProfileKey().get());
account.getProfileStore().storeProfileKey(recipientId, profileKey);
} catch (InvalidInputException e) {
logger.warn("Received invalid contact profile key from storage");
}
}
if (contactRecord.getIdentityKey().isPresent()) {
try {
final var identityKey = new IdentityKey(contactRecord.getIdentityKey().get());
account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date());
final var trustLevel = TrustLevel.fromIdentityState(contactRecord.getIdentityState());
if (trustLevel != null) {
account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identityKey, trustLevel);
}
} catch (InvalidKeyException e) {
logger.warn("Received invalid contact identity key from storage");
}
}
}
private void readGroupV1Record(final SignalStorageRecord record) {
if (record == null || !record.getGroupV1().isPresent()) {
return;
}
final var groupV1Record = record.getGroupV1().get();
final var groupIdV1 = GroupId.v1(groupV1Record.getGroupId());
final var group = account.getGroupStore().getGroup(groupIdV1);
if (group == null) {
try {
groupHelper.sendGroupInfoRequest(groupIdV1, account.getSelfRecipientId());
} catch (Throwable e) {
logger.warn("Failed to send group request", e);
}
}
final var groupV1 = account.getGroupStore().getOrCreateGroupV1(groupIdV1);
if (groupV1.isBlocked() != groupV1Record.isBlocked()) {
groupV1.setBlocked(groupV1Record.isBlocked());
account.getGroupStore().updateGroup(groupV1);
}
}
private void readGroupV2Record(final SignalStorageRecord record) {
if (record == null || !record.getGroupV2().isPresent()) {
return;
}
final var groupV2Record = record.getGroupV2().get();
if (groupV2Record.isArchived()) {
return;
}
final GroupMasterKey groupMasterKey;
try {
groupMasterKey = new GroupMasterKey(groupV2Record.getMasterKeyBytes());
} catch (InvalidInputException e) {
logger.warn("Received invalid group master key from storage");
return;
}
final var group = groupHelper.getOrMigrateGroup(groupMasterKey, 0, null);
if (group.isBlocked() != groupV2Record.isBlocked()) {
group.setBlocked(groupV2Record.isBlocked());
account.getGroupStore().updateGroup(group);
}
}
private void readAccountRecord(final SignalStorageManifest manifest) throws IOException {
Optional<StorageId> accountId = manifest.getAccountStorageId();
if (!accountId.isPresent()) {
logger.warn("Manifest has no account record, ignoring.");
return;
}
SignalStorageRecord record = getSignalStorageRecord(accountId.get());
if (record == null) {
logger.warn("Could not find account record, even though we had an ID, ignoring.");
return;
}
SignalAccountRecord accountRecord = record.getAccount().orNull();
if (accountRecord == null) {
logger.warn("The storage record didn't actually have an account, ignoring.");
return;
}
if (accountRecord.getProfileKey().isPresent()) {
try {
account.setProfileKey(new ProfileKey(accountRecord.getProfileKey().get()));
} catch (InvalidInputException e) {
logger.warn("Received invalid profile key from storage");
}
}
}
private SignalStorageRecord getSignalStorageRecord(final StorageId accountId) throws IOException {
List<SignalStorageRecord> records;
try {
records = dependencies.getAccountManager()
.readStorageRecords(account.getStorageKey(), Collections.singletonList(accountId));
} catch (InvalidKeyException e) {
logger.warn("Failed to read storage records, ignoring.");
return null;
}
return records.size() > 0 ? records.get(0) : null;
}
private List<SignalStorageRecord> getSignalStorageRecords(final List<StorageId> storageIds) throws IOException {
List<SignalStorageRecord> records;
try {
records = dependencies.getAccountManager().readStorageRecords(account.getStorageKey(), storageIds);
} catch (InvalidKeyException e) {
logger.warn("Failed to read storage records, ignoring.");
return List.of();
}
return records;
}
}

View file

@ -20,6 +20,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsI
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream; import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup; import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream; import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream;
import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage;
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
@ -215,6 +216,11 @@ public class SyncHelper {
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); sendHelper.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));
} }
public void sendKeysMessage() throws IOException {
var keysMessage = new KeysMessage(Optional.fromNullable(account.getStorageKey()));
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage));
}
public void handleSyncDeviceContacts(final InputStream input) throws IOException { public void handleSyncDeviceContacts(final InputStream input) throws IOException {
final var s = new DeviceContactsInputStream(input); final var s = new DeviceContactsInputStream(input);
DeviceContact c; DeviceContact c;

View file

@ -5,6 +5,7 @@ import org.asamk.signal.manager.StickerPackStore;
import org.asamk.signal.manager.helper.GroupHelper; import org.asamk.signal.manager.helper.GroupHelper;
import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.ProfileHelper;
import org.asamk.signal.manager.helper.SendHelper; import org.asamk.signal.manager.helper.SendHelper;
import org.asamk.signal.manager.helper.StorageHelper;
import org.asamk.signal.manager.helper.SyncHelper; import org.asamk.signal.manager.helper.SyncHelper;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
@ -17,6 +18,7 @@ public class Context {
private final GroupHelper groupHelper; private final GroupHelper groupHelper;
private final SyncHelper syncHelper; private final SyncHelper syncHelper;
private final ProfileHelper profileHelper; private final ProfileHelper profileHelper;
private final StorageHelper storageHelper;
public Context( public Context(
final SignalAccount account, final SignalAccount account,
@ -25,7 +27,8 @@ public class Context {
final SendHelper sendHelper, final SendHelper sendHelper,
final GroupHelper groupHelper, final GroupHelper groupHelper,
final SyncHelper syncHelper, final SyncHelper syncHelper,
final ProfileHelper profileHelper final ProfileHelper profileHelper,
final StorageHelper storageHelper
) { ) {
this.account = account; this.account = account;
this.dependencies = dependencies; this.dependencies = dependencies;
@ -34,6 +37,7 @@ public class Context {
this.groupHelper = groupHelper; this.groupHelper = groupHelper;
this.syncHelper = syncHelper; this.syncHelper = syncHelper;
this.profileHelper = profileHelper; this.profileHelper = profileHelper;
this.storageHelper = storageHelper;
} }
public SignalAccount getAccount() { public SignalAccount getAccount() {
@ -63,4 +67,8 @@ public class Context {
public ProfileHelper getProfileHelper() { public ProfileHelper getProfileHelper() {
return profileHelper; return profileHelper;
} }
public StorageHelper getStorageHelper() {
return storageHelper;
}
} }

View file

@ -24,6 +24,7 @@ import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientStore; import org.asamk.signal.manager.storage.recipients.RecipientStore;
import org.asamk.signal.manager.storage.senderKeys.SenderKeyStore;
import org.asamk.signal.manager.storage.sessions.SessionStore; import org.asamk.signal.manager.storage.sessions.SessionStore;
import org.asamk.signal.manager.storage.stickers.StickerStore; import org.asamk.signal.manager.storage.stickers.StickerStore;
import org.asamk.signal.manager.storage.threads.LegacyJsonThreadStore; import org.asamk.signal.manager.storage.threads.LegacyJsonThreadStore;
@ -83,6 +84,7 @@ public class SignalAccount implements Closeable {
private String registrationLockPin; private String registrationLockPin;
private MasterKey pinMasterKey; private MasterKey pinMasterKey;
private StorageKey storageKey; private StorageKey storageKey;
private long storageManifestVersion = -1;
private ProfileKey profileKey; private ProfileKey profileKey;
private int preKeyIdOffset; private int preKeyIdOffset;
private int nextSignedPreKeyId; private int nextSignedPreKeyId;
@ -95,6 +97,7 @@ public class SignalAccount implements Closeable {
private SignedPreKeyStore signedPreKeyStore; private SignedPreKeyStore signedPreKeyStore;
private SessionStore sessionStore; private SessionStore sessionStore;
private IdentityKeyStore identityKeyStore; private IdentityKeyStore identityKeyStore;
private SenderKeyStore senderKeyStore;
private GroupStore groupStore; private GroupStore groupStore;
private GroupStore.Storage groupStoreStorage; private GroupStore.Storage groupStoreStorage;
private RecipientStore recipientStore; private RecipientStore recipientStore;
@ -181,10 +184,15 @@ public class SignalAccount implements Closeable {
identityKey, identityKey,
registrationId, registrationId,
trustNewIdentity); trustNewIdentity);
senderKeyStore = new SenderKeyStore(getSharedSenderKeysFile(dataPath, username),
getSenderKeysPath(dataPath, username),
recipientStore::resolveRecipientAddress,
recipientStore);
signalProtocolStore = new SignalProtocolStore(preKeyStore, signalProtocolStore = new SignalProtocolStore(preKeyStore,
signedPreKeyStore, signedPreKeyStore,
sessionStore, sessionStore,
identityKeyStore, identityKeyStore,
senderKeyStore,
this::isMultiDevice); this::isMultiDevice);
messageCache = new MessageCache(getMessageCachePath(dataPath, username)); messageCache = new MessageCache(getMessageCachePath(dataPath, username));
@ -221,6 +229,7 @@ public class SignalAccount implements Closeable {
account.setProvisioningData(username, uuid, password, encryptedDeviceName, deviceId, profileKey); account.setProvisioningData(username, uuid, password, encryptedDeviceName, deviceId, profileKey);
account.recipientStore.resolveRecipientTrusted(account.getSelfAddress()); account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
account.sessionStore.archiveAllSessions(); account.sessionStore.archiveAllSessions();
account.senderKeyStore.deleteAll();
account.clearAllPreKeys(); account.clearAllPreKeys();
return account; return account;
} }
@ -283,6 +292,9 @@ public class SignalAccount implements Closeable {
this.registered = true; this.registered = true;
this.isMultiDevice = true; this.isMultiDevice = true;
this.lastReceiveTimestamp = 0; this.lastReceiveTimestamp = 0;
this.pinMasterKey = null;
this.storageManifestVersion = -1;
this.storageKey = null;
} }
private void migrateLegacyConfigs() { private void migrateLegacyConfigs() {
@ -303,6 +315,7 @@ public class SignalAccount implements Closeable {
identityKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId); identityKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId);
messageCache.mergeRecipients(recipientId, toBeMergedRecipientId); messageCache.mergeRecipients(recipientId, toBeMergedRecipientId);
groupStore.mergeRecipients(recipientId, toBeMergedRecipientId); groupStore.mergeRecipients(recipientId, toBeMergedRecipientId);
senderKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId);
} }
public static File getFileName(File dataPath, String username) { public static File getFileName(File dataPath, String username) {
@ -343,6 +356,14 @@ public class SignalAccount implements Closeable {
return new File(getUserPath(dataPath, username), "sessions"); return new File(getUserPath(dataPath, username), "sessions");
} }
private static File getSenderKeysPath(File dataPath, String username) {
return new File(getUserPath(dataPath, username), "sender-keys");
}
private static File getSharedSenderKeysFile(File dataPath, String username) {
return new File(getUserPath(dataPath, username), "shared-sender-keys-store");
}
private static File getRecipientsStoreFile(File dataPath, String username) { private static File getRecipientsStoreFile(File dataPath, String username) {
return new File(getUserPath(dataPath, username), "recipients-store"); return new File(getUserPath(dataPath, username), "recipients-store");
} }
@ -415,6 +436,9 @@ public class SignalAccount implements Closeable {
if (rootNode.hasNonNull("storageKey")) { if (rootNode.hasNonNull("storageKey")) {
storageKey = new StorageKey(Base64.getDecoder().decode(rootNode.get("storageKey").asText())); storageKey = new StorageKey(Base64.getDecoder().decode(rootNode.get("storageKey").asText()));
} }
if (rootNode.hasNonNull("storageManifestVersion")) {
storageManifestVersion = rootNode.get("storageManifestVersion").asLong();
}
if (rootNode.hasNonNull("preKeyIdOffset")) { if (rootNode.hasNonNull("preKeyIdOffset")) {
preKeyIdOffset = rootNode.get("preKeyIdOffset").asInt(0); preKeyIdOffset = rootNode.get("preKeyIdOffset").asInt(0);
} else { } else {
@ -676,6 +700,7 @@ public class SignalAccount implements Closeable {
pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize())) pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize()))
.put("storageKey", .put("storageKey",
storageKey == null ? null : Base64.getEncoder().encodeToString(storageKey.serialize())) storageKey == null ? null : Base64.getEncoder().encodeToString(storageKey.serialize()))
.put("storageManifestVersion", storageManifestVersion == -1 ? null : storageManifestVersion)
.put("preKeyIdOffset", preKeyIdOffset) .put("preKeyIdOffset", preKeyIdOffset)
.put("nextSignedPreKeyId", nextSignedPreKeyId) .put("nextSignedPreKeyId", nextSignedPreKeyId)
.put("profileKey", .put("profileKey",
@ -768,6 +793,10 @@ public class SignalAccount implements Closeable {
return stickerStore; return stickerStore;
} }
public SenderKeyStore getSenderKeyStore() {
return senderKeyStore;
}
public MessageCache getMessageCache() { public MessageCache getMessageCache() {
return messageCache; return messageCache;
} }
@ -797,6 +826,11 @@ public class SignalAccount implements Closeable {
return encryptedDeviceName; return encryptedDeviceName;
} }
public void setEncryptedDeviceName(final String encryptedDeviceName) {
this.encryptedDeviceName = encryptedDeviceName;
save();
}
public int getDeviceId() { public int getDeviceId() {
return deviceId; return deviceId;
} }
@ -851,6 +885,18 @@ public class SignalAccount implements Closeable {
save(); save();
} }
public long getStorageManifestVersion() {
return this.storageManifestVersion;
}
public void setStorageManifestVersion(final long storageManifestVersion) {
if (storageManifestVersion == this.storageManifestVersion) {
return;
}
this.storageManifestVersion = storageManifestVersion;
save();
}
public ProfileKey getProfileKey() { public ProfileKey getProfileKey() {
return profileKey; return profileKey;
} }
@ -922,6 +968,8 @@ public class SignalAccount implements Closeable {
public void finishRegistration(final UUID uuid, final MasterKey masterKey, final String pin) { public void finishRegistration(final UUID uuid, final MasterKey masterKey, final String pin) {
this.pinMasterKey = masterKey; this.pinMasterKey = masterKey;
this.storageManifestVersion = -1;
this.storageKey = null;
this.encryptedDeviceName = null; this.encryptedDeviceName = null;
this.deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; this.deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
this.isMultiDevice = false; this.isMultiDevice = false;
@ -932,6 +980,7 @@ public class SignalAccount implements Closeable {
save(); save();
getSessionStore().archiveAllSessions(); getSessionStore().archiveAllSessions();
senderKeyStore.deleteAll();
final var recipientId = getRecipientStore().resolveRecipientTrusted(getSelfAddress()); final var recipientId = getRecipientStore().resolveRecipientTrusted(getSelfAddress());
final var publicKey = getIdentityKeyPair().getPublicKey(); final var publicKey = getIdentityKeyPair().getPublicKey();
getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date()); getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date());

View file

@ -13,6 +13,7 @@ import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyStore; import org.whispersystems.libsignal.state.SignedPreKeyStore;
import org.whispersystems.signalservice.api.SignalServiceDataStore; import org.whispersystems.signalservice.api.SignalServiceDataStore;
import org.whispersystems.signalservice.api.SignalServiceSenderKeyStore;
import org.whispersystems.signalservice.api.SignalServiceSessionStore; import org.whispersystems.signalservice.api.SignalServiceSessionStore;
import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.push.DistributionId;
@ -28,6 +29,7 @@ public class SignalProtocolStore implements SignalServiceDataStore {
private final SignedPreKeyStore signedPreKeyStore; private final SignedPreKeyStore signedPreKeyStore;
private final SignalServiceSessionStore sessionStore; private final SignalServiceSessionStore sessionStore;
private final IdentityKeyStore identityKeyStore; private final IdentityKeyStore identityKeyStore;
private final SignalServiceSenderKeyStore senderKeyStore;
private final Supplier<Boolean> isMultiDevice; private final Supplier<Boolean> isMultiDevice;
public SignalProtocolStore( public SignalProtocolStore(
@ -35,12 +37,14 @@ public class SignalProtocolStore implements SignalServiceDataStore {
final SignedPreKeyStore signedPreKeyStore, final SignedPreKeyStore signedPreKeyStore,
final SignalServiceSessionStore sessionStore, final SignalServiceSessionStore sessionStore,
final IdentityKeyStore identityKeyStore, final IdentityKeyStore identityKeyStore,
final SignalServiceSenderKeyStore senderKeyStore,
final Supplier<Boolean> isMultiDevice final Supplier<Boolean> isMultiDevice
) { ) {
this.preKeyStore = preKeyStore; this.preKeyStore = preKeyStore;
this.signedPreKeyStore = signedPreKeyStore; this.signedPreKeyStore = signedPreKeyStore;
this.sessionStore = sessionStore; this.sessionStore = sessionStore;
this.identityKeyStore = identityKeyStore; this.identityKeyStore = identityKeyStore;
this.senderKeyStore = senderKeyStore;
this.isMultiDevice = isMultiDevice; this.isMultiDevice = isMultiDevice;
} }
@ -163,31 +167,29 @@ public class SignalProtocolStore implements SignalServiceDataStore {
public void storeSenderKey( public void storeSenderKey(
final SignalProtocolAddress sender, final UUID distributionId, final SenderKeyRecord record final SignalProtocolAddress sender, final UUID distributionId, final SenderKeyRecord record
) { ) {
// TODO senderKeyStore.storeSenderKey(sender, distributionId, record);
} }
@Override @Override
public SenderKeyRecord loadSenderKey(final SignalProtocolAddress sender, final UUID distributionId) { public SenderKeyRecord loadSenderKey(final SignalProtocolAddress sender, final UUID distributionId) {
// TODO return senderKeyStore.loadSenderKey(sender, distributionId);
return null;
} }
@Override @Override
public Set<SignalProtocolAddress> getSenderKeySharedWith(final DistributionId distributionId) { public Set<SignalProtocolAddress> getSenderKeySharedWith(final DistributionId distributionId) {
// TODO return senderKeyStore.getSenderKeySharedWith(distributionId);
return null;
} }
@Override @Override
public void markSenderKeySharedWith( public void markSenderKeySharedWith(
final DistributionId distributionId, final Collection<SignalProtocolAddress> addresses final DistributionId distributionId, final Collection<SignalProtocolAddress> addresses
) { ) {
// TODO senderKeyStore.markSenderKeySharedWith(distributionId, addresses);
} }
@Override @Override
public void clearSenderKeySharedWith(final Collection<SignalProtocolAddress> addresses) { public void clearSenderKeySharedWith(final Collection<SignalProtocolAddress> addresses) {
// TODO senderKeyStore.clearSenderKeySharedWith(addresses);
} }
@Override @Override

View file

@ -18,6 +18,7 @@ public class RecipientAddress {
* @param e164 The phone number of the user, if available. * @param e164 The phone number of the user, if available.
*/ */
public RecipientAddress(Optional<UUID> uuid, Optional<String> e164) { public RecipientAddress(Optional<UUID> uuid, Optional<String> e164) {
uuid = uuid.isPresent() && uuid.get().equals(UuidUtil.UNKNOWN_UUID) ? Optional.empty() : uuid;
if (!uuid.isPresent() && !e164.isPresent()) { if (!uuid.isPresent() && !e164.isPresent()) {
throw new AssertionError("Must have either a UUID or E164 number!"); throw new AssertionError("Must have either a UUID or E164 number!");
} }
@ -31,13 +32,11 @@ public class RecipientAddress {
} }
public RecipientAddress(SignalServiceAddress address) { public RecipientAddress(SignalServiceAddress address) {
this.uuid = Optional.of(address.getUuid()); this(Optional.of(address.getUuid()), Optional.ofNullable(address.getNumber().orNull()));
this.e164 = Optional.ofNullable(address.getNumber().orNull());
} }
public RecipientAddress(UUID uuid) { public RecipientAddress(UUID uuid) {
this.uuid = Optional.of(uuid); this(Optional.of(uuid), Optional.empty());
this.e164 = Optional.empty();
} }
public Optional<String> getNumber() { public Optional<String> getNumber() {

View file

@ -308,7 +308,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
final var byNumber = address.getNumber().isEmpty() final var byNumber = address.getNumber().isEmpty()
? Optional.<Recipient>empty() ? Optional.<Recipient>empty()
: findByNumberLocked(address.getNumber().get()); : findByNumberLocked(address.getNumber().get());
final var byUuid = address.getUuid().isEmpty() || address.getUuid().get().equals(UuidUtil.UNKNOWN_UUID) final var byUuid = address.getUuid().isEmpty()
? Optional.<Recipient>empty() ? Optional.<Recipient>empty()
: findByUuidLocked(address.getUuid().get()); : findByUuidLocked(address.getUuid().get());

View file

@ -0,0 +1,261 @@
package org.asamk.signal.manager.storage.senderKeys;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.asamk.signal.manager.util.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.groups.state.SenderKeyRecord;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups.state.SenderKeyStore {
private final static Logger logger = LoggerFactory.getLogger(SenderKeyRecordStore.class);
private final Map<Key, SenderKeyRecord> cachedSenderKeys = new HashMap<>();
private final File senderKeysPath;
private final RecipientResolver resolver;
public SenderKeyRecordStore(
final File senderKeysPath, final RecipientResolver resolver
) {
this.senderKeysPath = senderKeysPath;
this.resolver = resolver;
}
@Override
public SenderKeyRecord loadSenderKey(final SignalProtocolAddress address, final UUID distributionId) {
final var key = getKey(address, distributionId);
synchronized (cachedSenderKeys) {
return loadSenderKeyLocked(key);
}
}
@Override
public void storeSenderKey(
final SignalProtocolAddress address, final UUID distributionId, final SenderKeyRecord record
) {
final var key = getKey(address, distributionId);
synchronized (cachedSenderKeys) {
storeSenderKeyLocked(key, record);
}
}
public void deleteAll() {
synchronized (cachedSenderKeys) {
cachedSenderKeys.clear();
final var files = senderKeysPath.listFiles((_file, s) -> senderKeyFileNamePattern.matcher(s).matches());
if (files == null) {
return;
}
for (final var file : files) {
try {
Files.delete(file.toPath());
} catch (IOException e) {
logger.error("Failed to delete sender key file {}: {}", file, e.getMessage());
}
}
}
}
public void deleteAllFor(final RecipientId recipientId) {
synchronized (cachedSenderKeys) {
cachedSenderKeys.clear();
final var keys = getKeysLocked(recipientId);
for (var key : keys) {
deleteSenderKeyLocked(key);
}
}
}
public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
synchronized (cachedSenderKeys) {
final var keys = getKeysLocked(toBeMergedRecipientId);
final var otherHasSenderKeys = keys.size() > 0;
if (!otherHasSenderKeys) {
return;
}
logger.debug("Only to be merged recipient had sender keys, re-assigning to the new recipient.");
for (var key : keys) {
final var toBeMergedSenderKey = loadSenderKeyLocked(key);
deleteSenderKeyLocked(key);
if (toBeMergedSenderKey == null) {
continue;
}
final var newKey = new Key(recipientId, key.getDeviceId(), key.distributionId);
final var senderKeyRecord = loadSenderKeyLocked(newKey);
if (senderKeyRecord != null) {
continue;
}
storeSenderKeyLocked(newKey, senderKeyRecord);
}
}
}
/**
* @param identifier can be either a serialized uuid or a e164 phone number
*/
private RecipientId resolveRecipient(String identifier) {
return resolver.resolveRecipient(identifier);
}
private Key getKey(final SignalProtocolAddress address, final UUID distributionId) {
final var recipientId = resolveRecipient(address.getName());
return new Key(recipientId, address.getDeviceId(), distributionId);
}
private List<Key> getKeysLocked(RecipientId recipientId) {
final var files = senderKeysPath.listFiles((_file, s) -> s.startsWith(recipientId.getId() + "_"));
if (files == null) {
return List.of();
}
return parseFileNames(files);
}
final Pattern senderKeyFileNamePattern = Pattern.compile("([0-9]+)_([0-9]+)_([0-9a-z\\-]+)");
private List<Key> parseFileNames(final File[] files) {
return Arrays.stream(files)
.map(f -> senderKeyFileNamePattern.matcher(f.getName()))
.filter(Matcher::matches)
.map(matcher -> new Key(RecipientId.of(Long.parseLong(matcher.group(1))),
Integer.parseInt(matcher.group(2)),
UUID.fromString(matcher.group(3))))
.collect(Collectors.toList());
}
private File getSenderKeyFile(Key key) {
try {
IOUtils.createPrivateDirectories(senderKeysPath);
} catch (IOException e) {
throw new AssertionError("Failed to create sender keys path", e);
}
return new File(senderKeysPath,
key.getRecipientId().getId() + "_" + key.getDeviceId() + "_" + key.distributionId.toString());
}
private SenderKeyRecord loadSenderKeyLocked(final Key key) {
{
final var senderKeyRecord = cachedSenderKeys.get(key);
if (senderKeyRecord != null) {
return senderKeyRecord;
}
}
final var file = getSenderKeyFile(key);
if (!file.exists()) {
return null;
}
try (var inputStream = new FileInputStream(file)) {
final var senderKeyRecord = new SenderKeyRecord(inputStream.readAllBytes());
cachedSenderKeys.put(key, senderKeyRecord);
return senderKeyRecord;
} catch (IOException e) {
logger.warn("Failed to load sender key, resetting sender key: {}", e.getMessage());
return null;
}
}
private void storeSenderKeyLocked(final Key key, final SenderKeyRecord senderKeyRecord) {
cachedSenderKeys.put(key, senderKeyRecord);
final var file = getSenderKeyFile(key);
try {
try (var outputStream = new FileOutputStream(file)) {
outputStream.write(senderKeyRecord.serialize());
}
} catch (IOException e) {
logger.warn("Failed to store sender key, trying to delete file and retry: {}", e.getMessage());
try {
Files.delete(file.toPath());
try (var outputStream = new FileOutputStream(file)) {
outputStream.write(senderKeyRecord.serialize());
}
} catch (IOException e2) {
logger.error("Failed to store sender key file {}: {}", file, e2.getMessage());
}
}
}
private void deleteSenderKeyLocked(final Key key) {
cachedSenderKeys.remove(key);
final var file = getSenderKeyFile(key);
if (!file.exists()) {
return;
}
try {
Files.delete(file.toPath());
} catch (IOException e) {
logger.error("Failed to delete sender key file {}: {}", file, e.getMessage());
}
}
private static final class Key {
private final RecipientId recipientId;
private final int deviceId;
private final UUID distributionId;
public Key(
final RecipientId recipientId, final int deviceId, final UUID distributionId
) {
this.recipientId = recipientId;
this.deviceId = deviceId;
this.distributionId = distributionId;
}
public RecipientId getRecipientId() {
return recipientId;
}
public int getDeviceId() {
return deviceId;
}
public UUID getDistributionId() {
return distributionId;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final Key key = (Key) o;
if (deviceId != key.deviceId) return false;
if (!recipientId.equals(key.recipientId)) return false;
return distributionId.equals(key.distributionId);
}
@Override
public int hashCode() {
int result = recipientId.hashCode();
result = 31 * result + deviceId;
result = 31 * result + distributionId.hashCode();
return result;
}
}
}

View file

@ -0,0 +1,270 @@
package org.asamk.signal.manager.storage.senderKeys;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.asamk.signal.manager.helper.RecipientAddressResolver;
import org.asamk.signal.manager.storage.Utils;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.signalservice.api.push.DistributionId;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public class SenderKeySharedStore {
private final static Logger logger = LoggerFactory.getLogger(SenderKeySharedStore.class);
private final Map<DistributionId, Set<SenderKeySharedEntry>> sharedSenderKeys;
private final ObjectMapper objectMapper;
private final File file;
private final RecipientResolver resolver;
private final RecipientAddressResolver addressResolver;
public static SenderKeySharedStore load(
final File file, final RecipientAddressResolver addressResolver, final RecipientResolver resolver
) throws IOException {
final var objectMapper = Utils.createStorageObjectMapper();
try (var inputStream = new FileInputStream(file)) {
final var storage = objectMapper.readValue(inputStream, Storage.class);
final var sharedSenderKeys = new HashMap<DistributionId, Set<SenderKeySharedEntry>>();
for (final var senderKey : storage.sharedSenderKeys) {
final var entry = new SenderKeySharedEntry(RecipientId.of(senderKey.recipientId), senderKey.deviceId);
final var uuid = UuidUtil.parseOrNull(senderKey.distributionId);
if (uuid == null) {
logger.warn("Read invalid distribution id from storage {}, ignoring", senderKey.distributionId);
continue;
}
final var distributionId = DistributionId.from(uuid);
var entries = sharedSenderKeys.get(distributionId);
if (entries == null) {
entries = new HashSet<>();
}
entries.add(entry);
sharedSenderKeys.put(distributionId, entries);
}
return new SenderKeySharedStore(sharedSenderKeys, objectMapper, file, addressResolver, resolver);
} catch (FileNotFoundException e) {
logger.debug("Creating new shared sender key store.");
return new SenderKeySharedStore(new HashMap<>(), objectMapper, file, addressResolver, resolver);
}
}
private SenderKeySharedStore(
final Map<DistributionId, Set<SenderKeySharedEntry>> sharedSenderKeys,
final ObjectMapper objectMapper,
final File file,
final RecipientAddressResolver addressResolver,
final RecipientResolver resolver
) {
this.sharedSenderKeys = sharedSenderKeys;
this.objectMapper = objectMapper;
this.file = file;
this.addressResolver = addressResolver;
this.resolver = resolver;
}
public Set<SignalProtocolAddress> getSenderKeySharedWith(final DistributionId distributionId) {
synchronized (sharedSenderKeys) {
return sharedSenderKeys.get(distributionId)
.stream()
.map(k -> new SignalProtocolAddress(addressResolver.resolveRecipientAddress(k.getRecipientId())
.getIdentifier(), k.getDeviceId()))
.collect(Collectors.toSet());
}
}
public void markSenderKeySharedWith(
final DistributionId distributionId, final Collection<SignalProtocolAddress> addresses
) {
final var newEntries = addresses.stream()
.map(a -> new SenderKeySharedEntry(resolveRecipient(a.getName()), a.getDeviceId()))
.collect(Collectors.toSet());
synchronized (sharedSenderKeys) {
final var previousEntries = sharedSenderKeys.getOrDefault(distributionId, Set.of());
sharedSenderKeys.put(distributionId, new HashSet<>() {
{
addAll(previousEntries);
addAll(newEntries);
}
});
saveLocked();
}
}
public void clearSenderKeySharedWith(final Collection<SignalProtocolAddress> addresses) {
final var entriesToDelete = addresses.stream()
.map(a -> new SenderKeySharedEntry(resolveRecipient(a.getName()), a.getDeviceId()))
.collect(Collectors.toSet());
synchronized (sharedSenderKeys) {
for (final var distributionId : sharedSenderKeys.keySet()) {
final var entries = sharedSenderKeys.getOrDefault(distributionId, Set.of());
sharedSenderKeys.put(distributionId, new HashSet<>(entries) {
{
removeAll(entriesToDelete);
}
});
}
saveLocked();
}
}
public void deleteAll() {
synchronized (sharedSenderKeys) {
sharedSenderKeys.clear();
saveLocked();
}
}
public void deleteAllFor(final RecipientId recipientId) {
synchronized (sharedSenderKeys) {
for (final var distributionId : sharedSenderKeys.keySet()) {
final var entries = sharedSenderKeys.getOrDefault(distributionId, Set.of());
sharedSenderKeys.put(distributionId, new HashSet<>(entries) {
{
entries.removeIf(e -> e.getRecipientId().equals(recipientId));
}
});
}
saveLocked();
}
}
public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
synchronized (sharedSenderKeys) {
for (final var distributionId : sharedSenderKeys.keySet()) {
final var entries = sharedSenderKeys.getOrDefault(distributionId, Set.of());
sharedSenderKeys.put(distributionId,
entries.stream()
.map(e -> e.recipientId.equals(toBeMergedRecipientId) ? new SenderKeySharedEntry(
recipientId,
e.getDeviceId()) : e)
.collect(Collectors.toSet()));
}
saveLocked();
}
}
/**
* @param identifier can be either a serialized uuid or a e164 phone number
*/
private RecipientId resolveRecipient(String identifier) {
return resolver.resolveRecipient(identifier);
}
private void saveLocked() {
var storage = new Storage(sharedSenderKeys.entrySet().stream().flatMap(pair -> {
final var sharedWith = pair.getValue();
return sharedWith.stream()
.map(entry -> new Storage.SharedSenderKey(entry.getRecipientId().getId(),
entry.getDeviceId(),
pair.getKey().asUuid().toString()));
}).collect(Collectors.toList()));
// Write to memory first to prevent corrupting the file in case of serialization errors
try (var inMemoryOutput = new ByteArrayOutputStream()) {
objectMapper.writeValue(inMemoryOutput, storage);
var input = new ByteArrayInputStream(inMemoryOutput.toByteArray());
try (var outputStream = new FileOutputStream(file)) {
input.transferTo(outputStream);
}
} catch (Exception e) {
logger.error("Error saving shared sender key store file: {}", e.getMessage());
}
}
private static class Storage {
public List<SharedSenderKey> sharedSenderKeys;
// For deserialization
private Storage() {
}
public Storage(final List<SharedSenderKey> sharedSenderKeys) {
this.sharedSenderKeys = sharedSenderKeys;
}
private static class SharedSenderKey {
public long recipientId;
public int deviceId;
public String distributionId;
// For deserialization
private SharedSenderKey() {
}
public SharedSenderKey(final long recipientId, final int deviceId, final String distributionId) {
this.recipientId = recipientId;
this.deviceId = deviceId;
this.distributionId = distributionId;
}
}
}
private static final class SenderKeySharedEntry {
private final RecipientId recipientId;
private final int deviceId;
public SenderKeySharedEntry(
final RecipientId recipientId, final int deviceId
) {
this.recipientId = recipientId;
this.deviceId = deviceId;
}
public RecipientId getRecipientId() {
return recipientId;
}
public int getDeviceId() {
return deviceId;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final SenderKeySharedEntry that = (SenderKeySharedEntry) o;
if (deviceId != that.deviceId) return false;
return recipientId.equals(that.recipientId);
}
@Override
public int hashCode() {
int result = recipientId.hashCode();
result = 31 * result + deviceId;
return result;
}
}
}

View file

@ -0,0 +1,75 @@
package org.asamk.signal.manager.storage.senderKeys;
import org.asamk.signal.manager.helper.RecipientAddressResolver;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.groups.state.SenderKeyRecord;
import org.whispersystems.signalservice.api.SignalServiceSenderKeyStore;
import org.whispersystems.signalservice.api.push.DistributionId;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Set;
import java.util.UUID;
public class SenderKeyStore implements SignalServiceSenderKeyStore {
private final SenderKeyRecordStore senderKeyRecordStore;
private final SenderKeySharedStore senderKeySharedStore;
public SenderKeyStore(
final File file,
final File senderKeysPath,
final RecipientAddressResolver addressResolver,
final RecipientResolver resolver
) throws IOException {
this.senderKeyRecordStore = new SenderKeyRecordStore(senderKeysPath, resolver);
this.senderKeySharedStore = SenderKeySharedStore.load(file, addressResolver, resolver);
}
@Override
public void storeSenderKey(
final SignalProtocolAddress sender, final UUID distributionId, final SenderKeyRecord record
) {
senderKeyRecordStore.storeSenderKey(sender, distributionId, record);
}
@Override
public SenderKeyRecord loadSenderKey(final SignalProtocolAddress sender, final UUID distributionId) {
return senderKeyRecordStore.loadSenderKey(sender, distributionId);
}
@Override
public Set<SignalProtocolAddress> getSenderKeySharedWith(final DistributionId distributionId) {
return senderKeySharedStore.getSenderKeySharedWith(distributionId);
}
@Override
public void markSenderKeySharedWith(
final DistributionId distributionId, final Collection<SignalProtocolAddress> addresses
) {
senderKeySharedStore.markSenderKeySharedWith(distributionId, addresses);
}
@Override
public void clearSenderKeySharedWith(final Collection<SignalProtocolAddress> addresses) {
senderKeySharedStore.clearSenderKeySharedWith(addresses);
}
public void deleteAll() {
senderKeySharedStore.deleteAll();
senderKeyRecordStore.deleteAll();
}
public void rotateSenderKeys(RecipientId recipientId) {
senderKeySharedStore.deleteAllFor(recipientId);
senderKeyRecordStore.deleteAllFor(recipientId);
}
public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
senderKeySharedStore.mergeRecipients(recipientId, toBeMergedRecipientId);
senderKeyRecordStore.mergeRecipients(recipientId, toBeMergedRecipientId);
}
}

View file

@ -109,11 +109,7 @@ public class SessionStore implements SignalServiceSessionStore {
synchronized (cachedSessions) { synchronized (cachedSessions) {
final var session = loadSessionLocked(key); final var session = loadSessionLocked(key);
if (session == null) { return isActive(session);
return false;
}
return session.hasSenderChain() && session.getSessionVersion() == CiphertextMessage.CURRENT_VERSION;
} }
} }
@ -158,6 +154,7 @@ public class SessionStore implements SignalServiceSessionStore {
return recipientIdToNameMap.keySet() return recipientIdToNameMap.keySet()
.stream() .stream()
.flatMap(recipientId -> getKeysLocked(recipientId).stream()) .flatMap(recipientId -> getKeysLocked(recipientId).stream())
.filter(key -> isActive(this.loadSessionLocked(key)))
.map(key -> new SignalProtocolAddress(recipientIdToNameMap.get(key.recipientId), key.getDeviceId())) .map(key -> new SignalProtocolAddress(recipientIdToNameMap.get(key.recipientId), key.getDeviceId()))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
@ -182,7 +179,8 @@ public class SessionStore implements SignalServiceSessionStore {
public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) { public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
synchronized (cachedSessions) { synchronized (cachedSessions) {
final var otherHasSession = getKeysLocked(toBeMergedRecipientId).size() > 0; final var keys = getKeysLocked(toBeMergedRecipientId);
final var otherHasSession = keys.size() > 0;
if (!otherHasSession) { if (!otherHasSession) {
return; return;
} }
@ -192,8 +190,7 @@ public class SessionStore implements SignalServiceSessionStore {
logger.debug("To be merged recipient had sessions, deleting."); logger.debug("To be merged recipient had sessions, deleting.");
deleteAllSessions(toBeMergedRecipientId); deleteAllSessions(toBeMergedRecipientId);
} else { } else {
logger.debug("To be merged recipient had sessions, re-assigning to the new recipient."); logger.debug("Only to be merged recipient had sessions, re-assigning to the new recipient.");
final var keys = getKeysLocked(toBeMergedRecipientId);
for (var key : keys) { for (var key : keys) {
final var session = loadSessionLocked(key); final var session = loadSessionLocked(key);
deleteSessionLocked(key); deleteSessionLocked(key);
@ -321,6 +318,12 @@ public class SessionStore implements SignalServiceSessionStore {
} }
} }
private static boolean isActive(SessionRecord record) {
return record != null
&& record.hasSenderChain()
&& record.getSessionVersion() == CiphertextMessage.CURRENT_VERSION;
}
private static final class Key { private static final class Key {
private final RecipientId recipientId; private final RecipientId recipientId;

View file

@ -110,6 +110,9 @@ CAUTION: Only delete your account if you won't use this number again!
Update the account attributes on the signal server. Update the account attributes on the signal server.
Can fix problems with receiving messages. Can fix problems with receiving messages.
*-n* NAME, *--device-name* NAME::
Set a new device name for the main or linked device
=== setPin === setPin
Set a registration lock pin, to prevent others from registering this number. Set a registration lock pin, to prevent others from registering this number.

View file

@ -82,6 +82,12 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
DateUtils.formatTimestamp(content.getServerReceivedTimestamp()), DateUtils.formatTimestamp(content.getServerReceivedTimestamp()),
DateUtils.formatTimestamp(content.getServerDeliveredTimestamp())); DateUtils.formatTimestamp(content.getServerDeliveredTimestamp()));
if (content.getSenderKeyDistributionMessage().isPresent()) {
final var message = content.getSenderKeyDistributionMessage().get();
writer.println("Received a sender key distribution message for distributionId {}",
message.getDistributionId());
}
if (content.getDataMessage().isPresent()) { if (content.getDataMessage().isPresent()) {
var message = content.getDataMessage().get(); var message = content.getDataMessage().get();
printDataMessage(writer, message); printDataMessage(writer, message);
@ -235,6 +241,13 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
final var deviceId = callMessage.getDestinationDeviceId().get(); final var deviceId = callMessage.getDestinationDeviceId().get();
writer.println("Destination device id: {}", deviceId); writer.println("Destination device id: {}", deviceId);
} }
if (callMessage.getGroupId().isPresent()) {
final var groupId = GroupId.unknownVersion(callMessage.getGroupId().get());
writer.println("Destination group id: {}", groupId);
}
if (callMessage.getTimestamp().isPresent()) {
writer.println("Timestamp: {}", DateUtils.formatTimestamp(callMessage.getTimestamp().get()));
}
if (callMessage.getAnswerMessage().isPresent()) { if (callMessage.getAnswerMessage().isPresent()) {
var answerMessage = callMessage.getAnswerMessage().get(); var answerMessage = callMessage.getAnswerMessage().get();
writer.println("Answer message: {}, sdp: {})", answerMessage.getId(), answerMessage.getSdp()); writer.println("Answer message: {}, sdp: {})", answerMessage.getId(), answerMessage.getSdp());
@ -260,7 +273,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
} }
if (callMessage.getOpaqueMessage().isPresent()) { if (callMessage.getOpaqueMessage().isPresent()) {
final var opaqueMessage = callMessage.getOpaqueMessage().get(); final var opaqueMessage = callMessage.getOpaqueMessage().get();
writer.println("Opaque message: size {}", opaqueMessage.getOpaque().length); writer.println("Opaque message: size {}, urgency: {}",
opaqueMessage.getOpaque().length,
opaqueMessage.getUrgency().name());
} }
} }

View file

@ -75,6 +75,16 @@ public class JsonRpcDispatcherCommand implements LocalCommand {
objectMapper.valueToTree(s), objectMapper.valueToTree(s),
null)), m, ignoreAttachments); null)), m, ignoreAttachments);
// Maybe this should be handled inside the Manager
while (!m.hasCaughtUpWithOldMessages()) {
try {
synchronized (m) {
m.wait();
}
} catch (InterruptedException ignored) {
}
}
final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
final var jsonRpcReader = new JsonRpcReader(jsonRpcSender, () -> { final var jsonRpcReader = new JsonRpcReader(jsonRpcSender, () -> {

View file

@ -20,14 +20,16 @@ public class UpdateAccountCommand implements JsonRpcLocalCommand {
@Override @Override
public void attachToSubparser(final Subparser subparser) { public void attachToSubparser(final Subparser subparser) {
subparser.help("Update the account attributes on the signal server."); subparser.help("Update the account attributes on the signal server.");
subparser.addArgument("-n", "--device-name").help("Specify a name to describe this device.");
} }
@Override @Override
public void handleCommand( public void handleCommand(
final Namespace ns, final Manager m, final OutputWriter outputWriter final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException { ) throws CommandException {
var deviceName = ns.getString("device-name");
try { try {
m.updateAccountAttributes(); m.updateAccountAttributes(deviceName);
} catch (IOException e) { } catch (IOException e) {
throw new IOErrorException("UpdateAccount error: " + e.getMessage()); throw new IOErrorException("UpdateAccount error: " + e.getMessage());
} }