import org.asamk.signal.manager.helper.PinHelper;
import org.asamk.signal.manager.helper.ProfileHelper;
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.UnidentifiedAccessHelper;
import org.asamk.signal.manager.jobs.Context;
private final ProfileHelper profileHelper;
private final PinHelper pinHelper;
+ private final StorageHelper storageHelper;
private final SendHelper sendHelper;
private final SyncHelper syncHelper;
private final AttachmentHelper attachmentHelper;
avatarStore,
this::resolveSignalServiceAddress,
account.getRecipientStore());
+ this.storageHelper = new StorageHelper(account, dependencies, groupHelper);
this.contactHelper = new ContactHelper(account);
this.syncHelper = new SyncHelper(account,
attachmentHelper,
sendHelper,
groupHelper,
syncHelper,
- profileHelper);
+ profileHelper,
+ storageHelper);
var jobExecutor = new JobExecutor(context);
this.incomingMessageHandler = new IncomingMessageHandler(account,
public void requestAllSyncData() throws IOException {
syncHelper.requestAllSyncData();
+ retrieveRemoteStorage();
+ }
+
+ void retrieveRemoteStorage() throws IOException {
+ if (account.getStorageKey() != null) {
+ storageHelper.readDataFromStorage();
+ }
}
private byte[] getSenderCertificate() {
? null
: DeviceNameUtil.encryptDeviceName(deviceName, ret.getIdentity().getPrivateKey());
+ logger.debug("Finishing new device registration");
var deviceId = accountManager.finishNewDeviceRegistration(ret.getProvisioningCode(),
false,
true,
try {
m = new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent);
+ logger.debug("Refreshing pre keys");
try {
m.refreshPreKeys();
} catch (Exception e) {
throw e;
}
+ logger.debug("Requesting sync data");
try {
m.requestAllSyncData();
} catch (Exception e) {
masterKey = registrationLockData.getMasterKey();
}
- // TODO response.isStorageCapable()
//accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
account.finishRegistration(UuidUtil.parseOrNull(response.getUuid()), masterKey, pin);
m.refreshPreKeys();
// Set an initial empty profile so user can be added to groups
m.setProfile(null, null, null, null, null);
+ if (response.isStorageCapable()) {
+ m.retrieveRemoteStorage();
+ }
final var result = m;
m = null;
package org.asamk.signal.manager;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
+import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
public enum TrustLevel {
UNTRUSTED,
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) {
switch (verifiedState) {
case DEFAULT:
--- /dev/null
+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();
+ }
+ }
+ }
+}
--- /dev/null
+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();
+ }
+}
import org.asamk.signal.manager.actions.HandleAction;
import org.asamk.signal.manager.actions.RenewSessionAction;
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.SendGroupInfoRequestAction;
import org.asamk.signal.manager.actions.SendReceiptAction;
import org.asamk.signal.manager.actions.SendSyncBlockedListAction;
import org.asamk.signal.manager.actions.SendSyncContactsAction;
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.GroupNotFoundException;
import org.asamk.signal.manager.groups.GroupUtils;
if (rm.isBlockedListRequest()) {
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()) {
logger.warn("Received a group v1 sync message, that can't be handled anymore, ignoring.");
case LOCAL_PROFILE:
actions.add(new RetrieveProfileAction(account.getSelfRecipientId()));
case STORAGE_MANIFEST:
- // TODO
+ actions.add(RetrieveStorageDataAction.create());
}
}
if (syncMessage.getKeys().isPresent()) {
if (keysMessage.getStorageService().isPresent()) {
final var storageKey = keysMessage.getStorageService().get();
account.setStorageKey(storageKey);
+ actions.add(RetrieveStorageDataAction.create());
}
}
if (syncMessage.getConfiguration().isPresent()) {
--- /dev/null
+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;
+ }
+}
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup;
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.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.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 {
final var s = new DeviceContactsInputStream(input);
DeviceContact c;
import org.asamk.signal.manager.helper.GroupHelper;
import org.asamk.signal.manager.helper.ProfileHelper;
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.storage.SignalAccount;
private final GroupHelper groupHelper;
private final SyncHelper syncHelper;
private final ProfileHelper profileHelper;
+ private final StorageHelper storageHelper;
public Context(
final SignalAccount account,
final SendHelper sendHelper,
final GroupHelper groupHelper,
final SyncHelper syncHelper,
- final ProfileHelper profileHelper
+ final ProfileHelper profileHelper,
+ final StorageHelper storageHelper
) {
this.account = account;
this.dependencies = dependencies;
this.groupHelper = groupHelper;
this.syncHelper = syncHelper;
this.profileHelper = profileHelper;
+ this.storageHelper = storageHelper;
}
public SignalAccount getAccount() {
public ProfileHelper getProfileHelper() {
return profileHelper;
}
+
+ public StorageHelper getStorageHelper() {
+ return storageHelper;
+ }
}
private String registrationLockPin;
private MasterKey pinMasterKey;
private StorageKey storageKey;
+ private long storageManifestVersion = -1;
private ProfileKey profileKey;
private int preKeyIdOffset;
private int nextSignedPreKeyId;
this.registered = true;
this.isMultiDevice = true;
this.lastReceiveTimestamp = 0;
+ this.pinMasterKey = null;
+ this.storageManifestVersion = -1;
+ this.storageKey = null;
}
private void migrateLegacyConfigs() {
if (rootNode.hasNonNull("storageKey")) {
storageKey = new StorageKey(Base64.getDecoder().decode(rootNode.get("storageKey").asText()));
}
+ if (rootNode.hasNonNull("storageManifestVersion")) {
+ storageManifestVersion = rootNode.get("storageManifestVersion").asLong();
+ }
if (rootNode.hasNonNull("preKeyIdOffset")) {
preKeyIdOffset = rootNode.get("preKeyIdOffset").asInt(0);
} else {
pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize()))
.put("storageKey",
storageKey == null ? null : Base64.getEncoder().encodeToString(storageKey.serialize()))
+ .put("storageManifestVersion", storageManifestVersion == -1 ? null : storageManifestVersion)
.put("preKeyIdOffset", preKeyIdOffset)
.put("nextSignedPreKeyId", nextSignedPreKeyId)
.put("profileKey",
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() {
return profileKey;
}
public void finishRegistration(final UUID uuid, final MasterKey masterKey, final String pin) {
this.pinMasterKey = masterKey;
+ this.storageManifestVersion = -1;
+ this.storageKey = null;
this.encryptedDeviceName = null;
this.deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
this.isMultiDevice = false;