mirror of
https://github.com/AsamK/signal-cli
synced 2025-09-04 05:00:39 +00:00
Merge branch 'AsamK:master' into master
This commit is contained in:
commit
f2c7c60669
29 changed files with 1146 additions and 86 deletions
|
@ -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
|
||||||
|
|
|
@ -2272,7 +2272,6 @@
|
||||||
"fields":[
|
"fields":[
|
||||||
{"name":"bitField0_"},
|
{"name":"bitField0_"},
|
||||||
{"name":"e164_"},
|
{"name":"e164_"},
|
||||||
{"name":"relay_"},
|
|
||||||
{"name":"uuid_"}
|
{"name":"uuid_"}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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());
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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, () -> {
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue