Refactor group store

This commit is contained in:
AsamK 2021-05-02 16:02:54 +02:00
parent a1014ba39c
commit 5b8c0c4e2d
14 changed files with 644 additions and 473 deletions

View file

@ -265,6 +265,10 @@ public class Manager implements Closeable {
return account.getSelfAddress();
}
public RecipientId getSelfRecipientId() {
return account.getSelfRecipientId();
}
private IdentityKeyPair getIdentityKeyPair() {
return account.getIdentityKeyPair();
}
@ -673,7 +677,7 @@ public class Manager implements Closeable {
if (g == null) {
throw new GroupNotFoundException(groupId);
}
if (!g.isMember(account.getSelfAddress())) {
if (!g.isMember(account.getSelfRecipientId())) {
throw new NotAGroupMemberException(groupId, g.getTitle());
}
return g;
@ -684,7 +688,7 @@ public class Manager implements Closeable {
if (g == null) {
throw new GroupNotFoundException(groupId);
}
if (!g.isMember(account.getSelfAddress()) && !g.isPendingMember(account.getSelfAddress())) {
if (!g.isMember(account.getSelfRecipientId()) && !g.isPendingMember(account.getSelfRecipientId())) {
throw new NotAGroupMemberException(groupId, g.getTitle());
}
return g;
@ -725,7 +729,7 @@ public class Manager implements Closeable {
GroupUtils.setGroupContext(messageBuilder, g);
messageBuilder.withExpiration(g.getMessageExpirationTime());
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId()));
}
public Pair<Long, List<SendMessageResult>> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException {
@ -736,17 +740,17 @@ public class Manager implements Closeable {
var groupInfoV1 = (GroupInfoV1) g;
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT).withId(groupId.serialize()).build();
messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group);
groupInfoV1.removeMember(account.getSelfAddress());
groupInfoV1.removeMember(account.getSelfRecipientId());
account.getGroupStore().updateGroup(groupInfoV1);
} else {
final var groupInfoV2 = (GroupInfoV2) g;
final var groupGroupChangePair = groupHelper.leaveGroup(groupInfoV2);
groupInfoV2.setGroup(groupGroupChangePair.first());
groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient);
messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray());
account.getGroupStore().updateGroup(groupInfoV2);
}
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId()));
}
public Pair<GroupId, List<SendMessageResult>> updateGroup(
@ -754,11 +758,7 @@ public class Manager implements Closeable {
) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
return sendUpdateGroupMessage(groupId,
name,
members == null
? null
: getSignalServiceAddresses(members).stream()
.map(this::resolveRecipient)
.collect(Collectors.toSet()),
members == null ? null : getSignalServiceAddresses(members),
avatarFile);
}
@ -769,16 +769,20 @@ public class Manager implements Closeable {
SignalServiceDataMessage.Builder messageBuilder;
if (groupId == null) {
// Create new group
var gv2 = groupHelper.createGroupV2(name == null ? "" : name,
var gv2Pair = groupHelper.createGroupV2(name == null ? "" : name,
members == null ? Set.of() : members,
avatarFile);
if (gv2 == null) {
if (gv2Pair == null) {
var gv1 = new GroupInfoV1(GroupIdV1.createRandom());
gv1.addMembers(List.of(account.getSelfAddress()));
gv1.addMembers(List.of(account.getSelfRecipientId()));
updateGroupV1(gv1, name, members, avatarFile);
messageBuilder = getGroupUpdateMessageBuilder(gv1);
g = gv1;
} else {
final var gv2 = gv2Pair.first();
final var decryptedGroup = gv2Pair.second();
gv2.setGroup(decryptedGroup, this::resolveRecipient);
if (avatarFile != null) {
avatarStore.storeGroupAvatar(gv2.getGroupId(),
outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
@ -792,7 +796,7 @@ public class Manager implements Closeable {
final var groupInfoV2 = (GroupInfoV2) group;
Pair<Long, List<SendMessageResult>> result = null;
if (groupInfoV2.isPendingMember(getSelfAddress())) {
if (groupInfoV2.isPendingMember(account.getSelfRecipientId())) {
var groupGroupChangePair = groupHelper.acceptInvite(groupInfoV2);
result = sendUpdateGroupMessage(groupInfoV2,
groupGroupChangePair.first(),
@ -801,10 +805,7 @@ public class Manager implements Closeable {
if (members != null) {
final var newMembers = new HashSet<>(members);
newMembers.removeAll(group.getMembers()
.stream()
.map(this::resolveRecipient)
.collect(Collectors.toSet()));
newMembers.removeAll(group.getMembers());
if (newMembers.size() > 0) {
var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, newMembers);
result = sendUpdateGroupMessage(groupInfoV2,
@ -834,7 +835,8 @@ public class Manager implements Closeable {
account.getGroupStore().updateGroup(g);
final var result = sendMessage(messageBuilder, g.getMembersIncludingPendingWithout(account.getSelfAddress()));
final var result = sendMessage(messageBuilder,
g.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
return new Pair<>(g.getGroupId(), result.second());
}
@ -846,12 +848,13 @@ public class Manager implements Closeable {
}
if (members != null) {
final var memberAddresses = members.stream()
final var newMemberAddresses = members.stream()
.filter(member -> !g.isMember(member))
.map(this::resolveSignalServiceAddress)
.collect(Collectors.toList());
final var newE164Members = new HashSet<String>();
for (var member : memberAddresses) {
if (g.isMember(member) || !member.getNumber().isPresent()) {
for (var member : newMemberAddresses) {
if (!member.getNumber().isPresent()) {
continue;
}
newE164Members.add(member.getNumber().get());
@ -866,7 +869,7 @@ public class Manager implements Closeable {
+ " to group: Not registered on Signal");
}
g.addMembers(memberAddresses);
g.addMembers(members);
}
if (avatarFile != null) {
@ -928,10 +931,10 @@ public class Manager implements Closeable {
private Pair<Long, List<SendMessageResult>> sendUpdateGroupMessage(
GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
) throws IOException {
group.setGroup(newDecryptedGroup);
group.setGroup(newDecryptedGroup, this::resolveRecipient);
final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray());
account.getGroupStore().updateGroup(group);
return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfAddress()));
return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
}
Pair<Long, List<SendMessageResult>> sendGroupInfoMessage(
@ -944,21 +947,24 @@ public class Manager implements Closeable {
}
g = (GroupInfoV1) group;
if (!g.isMember(recipient)) {
if (!g.isMember(resolveRecipient(recipient))) {
throw new NotAGroupMemberException(groupId, g.name);
}
var messageBuilder = getGroupUpdateMessageBuilder(g);
// Send group message only to the recipient who requested it
return sendMessage(messageBuilder, List.of(recipient));
return sendMessage(messageBuilder, Set.of(resolveRecipient(recipient)));
}
private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException {
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
.withId(g.getGroupId().serialize())
.withName(g.name)
.withMembers(new ArrayList<>(g.getMembers()));
.withMembers(g.getMembers()
.stream()
.map(this::resolveSignalServiceAddress)
.collect(Collectors.toList()));
try {
final var attachment = createGroupAvatarAttachment(g.getGroupId());
@ -991,7 +997,7 @@ public class Manager implements Closeable {
var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build());
// Send group info request message to the recipient who sent us a message with this groupId
return sendMessage(messageBuilder, List.of(recipient));
return sendMessage(messageBuilder, Set.of(resolveRecipient(recipient)));
}
void sendReceipt(
@ -1126,9 +1132,9 @@ public class Manager implements Closeable {
.storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build());
}
private void sendExpirationTimerUpdate(SignalServiceAddress address) throws IOException {
private void sendExpirationTimerUpdate(RecipientId recipientId) throws IOException {
final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
sendMessage(messageBuilder, List.of(address));
sendMessage(messageBuilder, Set.of(recipientId));
}
/**
@ -1139,7 +1145,7 @@ public class Manager implements Closeable {
) throws IOException, InvalidNumberException {
var recipientId = canonicalizeAndResolveRecipient(number);
setExpirationTimer(recipientId, messageExpirationTimer);
sendExpirationTimerUpdate(resolveSignalServiceAddress(recipientId));
sendExpirationTimerUpdate(recipientId);
account.save();
}
@ -1266,7 +1272,7 @@ public class Manager implements Closeable {
messageSender.sendMessage(message, unidentifiedAccessHelper.getAccessForSync());
}
private Collection<SignalServiceAddress> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException {
private Set<RecipientId> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException {
final var signalServiceAddresses = new HashSet<SignalServiceAddress>(numbers.size());
final var addressesMissingUuid = new HashSet<SignalServiceAddress>();
@ -1302,7 +1308,7 @@ public class Manager implements Closeable {
}
}
return signalServiceAddresses;
return signalServiceAddresses.stream().map(this::resolveRecipient).collect(Collectors.toSet());
}
private Map<String, UUID> getRegisteredUsers(final Set<String> numbersMissingUuid) throws IOException {
@ -1316,10 +1322,8 @@ public class Manager implements Closeable {
}
private Pair<Long, List<SendMessageResult>> sendMessage(
SignalServiceDataMessage.Builder messageBuilder, Collection<SignalServiceAddress> recipients
SignalServiceDataMessage.Builder messageBuilder, Set<RecipientId> recipientIds
) throws IOException {
recipients = recipients.stream().map(this::resolveSignalServiceAddress).collect(Collectors.toSet());
final var recipientIds = recipients.stream().map(this::resolveRecipient).collect(Collectors.toSet());
final var timestamp = System.currentTimeMillis();
messageBuilder.withTimestamp(timestamp);
getOrCreateMessagePipe();
@ -1331,8 +1335,12 @@ public class Manager implements Closeable {
try {
var messageSender = createMessageSender();
final var isRecipientUpdate = false;
var result = messageSender.sendMessage(new ArrayList<>(recipients),
unidentifiedAccessHelper.getAccessFor(recipientIds),
final var recipientIdList = new ArrayList<>(recipientIds);
final var addresses = recipientIdList.stream()
.map(this::resolveSignalServiceAddress)
.collect(Collectors.toList());
var result = messageSender.sendMessage(addresses,
unidentifiedAccessHelper.getAccessFor(recipientIdList),
isRecipientUpdate,
message);
@ -1352,19 +1360,19 @@ public class Manager implements Closeable {
} else {
// Send to all individually, so sync messages are sent correctly
messageBuilder.withProfileKey(account.getProfileKey().serialize());
var results = new ArrayList<SendMessageResult>(recipients.size());
for (var address : recipients) {
final var contact = account.getContactStore().getContact(resolveRecipient(address));
var results = new ArrayList<SendMessageResult>(recipientIds.size());
for (var recipientId : recipientIds) {
final var contact = account.getContactStore().getContact(recipientId);
final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
messageBuilder.withExpiration(expirationTime);
message = messageBuilder.build();
results.add(sendMessage(address, message));
results.add(sendMessage(resolveSignalServiceAddress(recipientId), message));
}
return new Pair<>(timestamp, results);
}
} finally {
if (message != null && message.isEndSession()) {
for (var recipient : recipients) {
for (var recipient : recipientIds) {
handleEndSession(recipient);
}
}
@ -1448,8 +1456,8 @@ public class Manager implements Closeable {
}
}
private void handleEndSession(SignalServiceAddress source) {
account.getSessionStore().deleteAllSessions(source.getIdentifier());
private void handleEndSession(RecipientId recipientId) {
account.getSessionStore().deleteAllSessions(recipientId);
}
private List<HandleAction> handleSignalServiceDataMessage(
@ -1486,7 +1494,7 @@ public class Manager implements Closeable {
groupV1.addMembers(groupInfo.getMembers()
.get()
.stream()
.map(this::resolveSignalServiceAddress)
.map(this::resolveRecipient)
.collect(Collectors.toSet()));
}
@ -1500,7 +1508,7 @@ public class Manager implements Closeable {
break;
case QUIT: {
if (groupV1 != null) {
groupV1.removeMember(source);
groupV1.removeMember(resolveRecipient(source));
account.getGroupStore().updateGroup(groupV1);
}
break;
@ -1527,7 +1535,7 @@ public class Manager implements Closeable {
final var conversationPartnerAddress = isSync ? destination : source;
if (conversationPartnerAddress != null && message.isEndSession()) {
handleEndSession(conversationPartnerAddress);
handleEndSession(resolveRecipient(conversationPartnerAddress));
}
if (message.isExpirationUpdate() || message.getBody().isPresent()) {
if (message.getGroupContext().isPresent()) {
@ -1613,7 +1621,7 @@ public class Manager implements Closeable {
final GroupInfoV2 groupInfoV2;
if (groupInfo instanceof GroupInfoV1) {
// Received a v2 group message for a v1 group, we need to locally migrate the group
account.getGroupStore().deleteGroup(groupInfo.getGroupId());
account.getGroupStore().deleteGroupV1(((GroupInfoV1) groupInfo).getGroupId());
groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
logger.info("Locally migrated group {} to group v2, id: {}",
groupInfo.getGroupId().toBase64(),
@ -1641,7 +1649,7 @@ public class Manager implements Closeable {
downloadGroupAvatar(groupId, groupSecretParams, avatar);
}
}
groupInfoV2.setGroup(group);
groupInfoV2.setGroup(group, this::resolveRecipient);
account.getGroupStore().updateGroup(groupInfoV2);
}
@ -1885,7 +1893,7 @@ public class Manager implements Closeable {
}
var groupId = GroupUtils.getGroupId(message.getGroupContext().get());
var group = getGroup(groupId);
if (group != null && !group.isMember(source)) {
if (group != null && !group.isMember(resolveRecipient(source))) {
return true;
}
}
@ -1959,13 +1967,13 @@ public class Manager implements Closeable {
}
syncGroup.addMembers(g.getMembers()
.stream()
.map(this::resolveSignalServiceAddress)
.map(this::resolveRecipient)
.collect(Collectors.toSet()));
if (!g.isActive()) {
syncGroup.removeMember(account.getSelfAddress());
syncGroup.removeMember(account.getSelfRecipientId());
} else {
// Add ourself to the member set as it's marked as active
syncGroup.addMembers(List.of(account.getSelfAddress()));
syncGroup.addMembers(List.of(account.getSelfRecipientId()));
}
syncGroup.blocked = g.isBlocked();
if (g.getColor().isPresent()) {
@ -1975,7 +1983,6 @@ public class Manager implements Closeable {
if (g.getAvatar().isPresent()) {
downloadGroupAvatar(g.getAvatar().get(), syncGroup.getGroupId());
}
syncGroup.inboxPosition = g.getInboxPosition().orNull();
syncGroup.archived = g.isArchived();
account.getGroupStore().updateGroup(syncGroup);
}
@ -2282,13 +2289,16 @@ public class Manager implements Closeable {
var groupInfo = (GroupInfoV1) record;
out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
Optional.fromNullable(groupInfo.name),
new ArrayList<>(groupInfo.getMembers()),
groupInfo.getMembers()
.stream()
.map(this::resolveSignalServiceAddress)
.collect(Collectors.toList()),
createGroupAvatarAttachment(groupInfo.getGroupId()),
groupInfo.isMember(account.getSelfAddress()),
groupInfo.isMember(account.getSelfRecipientId()),
Optional.of(groupInfo.messageExpirationTime),
Optional.fromNullable(groupInfo.color),
groupInfo.blocked,
Optional.fromNullable(groupInfo.inboxPosition),
Optional.absent(),
groupInfo.archived));
}
}
@ -2434,7 +2444,7 @@ public class Manager implements Closeable {
final var group = account.getGroupStore().getGroup(groupId);
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey());
((GroupInfoV2) group).setGroup(groupHelper.getDecryptedGroup(groupSecretParams));
((GroupInfoV2) group).setGroup(groupHelper.getDecryptedGroup(groupSecretParams), this::resolveRecipient);
account.getGroupStore().updateGroup(group);
}
return group;

View file

@ -1,5 +1,7 @@
package org.asamk.signal.manager.groups;
import java.util.Base64;
import static org.asamk.signal.manager.util.KeyUtils.getSecretBytes;
public class GroupIdV1 extends GroupId {
@ -8,6 +10,10 @@ public class GroupIdV1 extends GroupId {
return new GroupIdV1(getSecretBytes(16));
}
public static GroupIdV1 fromBase64(String groupId) {
return new GroupIdV1(Base64.getDecoder().decode(groupId));
}
public GroupIdV1(final byte[] id) {
super(id);
}

View file

@ -100,7 +100,7 @@ public class GroupHelper {
groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
}
public GroupInfoV2 createGroupV2(
public Pair<GroupInfoV2, DecryptedGroup> createGroupV2(
String name, Set<RecipientId> members, File avatarFile
) throws IOException {
final var avatarBytes = readAvatarBytes(avatarFile);
@ -129,9 +129,8 @@ public class GroupHelper {
final var groupId = GroupUtils.getGroupIdV2(groupSecretParams);
final var masterKey = groupSecretParams.getMasterKey();
var g = new GroupInfoV2(groupId, masterKey);
g.setGroup(decryptedGroup);
return g;
return new Pair<>(g, decryptedGroup);
}
private byte[] readAvatarBytes(final File avatarFile) throws IOException {

View file

@ -6,7 +6,6 @@ import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@ -76,7 +75,7 @@ public class UnidentifiedAccessHelper {
}
}
public List<Optional<UnidentifiedAccessPair>> getAccessFor(Collection<RecipientId> recipients) {
public List<Optional<UnidentifiedAccessPair>> getAccessFor(List<RecipientId> recipients) {
return recipients.stream().map(this::getAccessFor).collect(Collectors.toList());
}

View file

@ -7,7 +7,7 @@ import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.storage.contacts.ContactsStore;
import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
import org.asamk.signal.manager.storage.groups.JsonGroupStore;
import org.asamk.signal.manager.storage.groups.GroupStore;
import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
import org.asamk.signal.manager.storage.messageCache.MessageCache;
import org.asamk.signal.manager.storage.prekeys.PreKeyStore;
@ -86,7 +86,8 @@ public class SignalAccount implements Closeable {
private SignedPreKeyStore signedPreKeyStore;
private SessionStore sessionStore;
private IdentityKeyStore identityKeyStore;
private JsonGroupStore groupStore;
private GroupStore groupStore;
private GroupStore.Storage groupStoreStorage;
private RecipientStore recipientStore;
private StickerStore stickerStore;
private StickerStore.Storage stickerStoreStorage;
@ -130,7 +131,9 @@ public class SignalAccount implements Closeable {
account.profileKey = profileKey;
account.initStores(dataPath, identityKey, registrationId);
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
account.groupStore = new GroupStore(getGroupCachePath(dataPath, username),
account.recipientStore::resolveRecipient,
account::saveGroupStore);
account.stickerStore = new StickerStore(account::saveStickerStore);
account.registered = false;
@ -183,7 +186,9 @@ public class SignalAccount implements Closeable {
account.deviceId = deviceId;
account.initStores(dataPath, identityKey, registrationId);
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
account.groupStore = new GroupStore(getGroupCachePath(dataPath, username),
account.recipientStore::resolveRecipient,
account::saveGroupStore);
account.stickerStore = new StickerStore(account::saveStickerStore);
account.registered = true;
@ -209,6 +214,7 @@ public class SignalAccount implements Closeable {
sessionStore.mergeRecipients(recipientId, toBeMergedRecipientId);
identityKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId);
messageCache.mergeRecipients(recipientId, toBeMergedRecipientId);
groupStore.mergeRecipients(recipientId, toBeMergedRecipientId);
}
public static File getFileName(File dataPath, String username) {
@ -331,13 +337,16 @@ public class SignalAccount implements Closeable {
loadLegacyStores(rootNode, legacySignalProtocolStore);
var groupStoreNode = rootNode.get("groupStore");
if (groupStoreNode != null) {
groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
groupStore.groupCachePath = getGroupCachePath(dataPath, username);
}
if (groupStore == null) {
groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
if (rootNode.hasNonNull("groupStore")) {
groupStoreStorage = jsonProcessor.convertValue(rootNode.get("groupStore"), GroupStore.Storage.class);
groupStore = GroupStore.fromStorage(groupStoreStorage,
getGroupCachePath(dataPath, username),
recipientStore::resolveRecipient,
this::saveGroupStore);
} else {
groupStore = new GroupStore(getGroupCachePath(dataPath, username),
recipientStore::resolveRecipient,
this::saveGroupStore);
}
if (rootNode.hasNonNull("stickerStore")) {
@ -510,6 +519,11 @@ public class SignalAccount implements Closeable {
save();
}
private void saveGroupStore(GroupStore.Storage storage) {
this.groupStoreStorage = storage;
save();
}
public void save() {
synchronized (fileChannel) {
var rootNode = jsonProcessor.createObjectNode();
@ -534,7 +548,7 @@ public class SignalAccount implements Closeable {
.put("nextSignedPreKeyId", nextSignedPreKeyId)
.put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
.put("registered", registered)
.putPOJO("groupStore", groupStore)
.putPOJO("groupStore", groupStoreStorage)
.putPOJO("stickerStore", stickerStoreStorage);
try {
try (var output = new ByteArrayOutputStream()) {
@ -597,7 +611,7 @@ public class SignalAccount implements Closeable {
return identityKeyStore;
}
public JsonGroupStore getGroupStore() {
public GroupStore getGroupStore() {
return groupStore;
}

View file

@ -17,15 +17,15 @@ public class Utils {
}
public static ObjectMapper createStorageObjectMapper() {
final ObjectMapper jsonProcessor = new ObjectMapper();
final ObjectMapper objectMapper = new ObjectMapper();
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.PUBLIC_ONLY);
jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print
jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.PUBLIC_ONLY);
objectMapper.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
objectMapper.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
objectMapper.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
return jsonProcessor;
return objectMapper;
}
public static JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {

View file

@ -1,10 +1,8 @@
package org.asamk.signal.manager.storage.groups;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import java.util.Set;
import java.util.stream.Collectors;
@ -12,66 +10,43 @@ import java.util.stream.Stream;
public abstract class GroupInfo {
@JsonIgnore
public abstract GroupId getGroupId();
@JsonIgnore
public abstract String getTitle();
@JsonIgnore
public abstract GroupInviteLinkUrl getGroupInviteLink();
@JsonIgnore
public abstract Set<SignalServiceAddress> getMembers();
public abstract Set<RecipientId> getMembers();
@JsonIgnore
public Set<SignalServiceAddress> getPendingMembers() {
public Set<RecipientId> getPendingMembers() {
return Set.of();
}
@JsonIgnore
public Set<SignalServiceAddress> getRequestingMembers() {
public Set<RecipientId> getRequestingMembers() {
return Set.of();
}
@JsonIgnore
public abstract boolean isBlocked();
@JsonIgnore
public abstract void setBlocked(boolean blocked);
@JsonIgnore
public abstract int getMessageExpirationTime();
@JsonIgnore
public Set<SignalServiceAddress> getMembersWithout(SignalServiceAddress address) {
return getMembers().stream().filter(member -> !member.matches(address)).collect(Collectors.toSet());
public Set<RecipientId> getMembersWithout(RecipientId recipientId) {
return getMembers().stream().filter(member -> !member.equals(recipientId)).collect(Collectors.toSet());
}
@JsonIgnore
public Set<SignalServiceAddress> getMembersIncludingPendingWithout(SignalServiceAddress address) {
public Set<RecipientId> getMembersIncludingPendingWithout(RecipientId recipientId) {
return Stream.concat(getMembers().stream(), getPendingMembers().stream())
.filter(member -> !member.matches(address))
.filter(member -> !member.equals(recipientId))
.collect(Collectors.toSet());
}
@JsonIgnore
public boolean isMember(SignalServiceAddress address) {
for (var member : getMembers()) {
if (member.matches(address)) {
return true;
}
}
return false;
public boolean isMember(RecipientId recipientId) {
return getMembers().contains(recipientId);
}
@JsonIgnore
public boolean isPendingMember(SignalServiceAddress address) {
for (var member : getPendingMembers()) {
if (member.matches(address)) {
return true;
}
}
return false;
public boolean isPendingMember(RecipientId recipientId) {
return getPendingMembers().contains(recipientId);
}
}

View file

@ -1,55 +1,27 @@
package org.asamk.signal.manager.storage.groups;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupIdV1;
import org.asamk.signal.manager.groups.GroupIdV2;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.asamk.signal.manager.groups.GroupUtils;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class GroupInfoV1 extends GroupInfo {
private static final ObjectMapper jsonProcessor = new ObjectMapper();
private final GroupIdV1 groupId;
private GroupIdV2 expectedV2Id;
@JsonProperty
public String name;
@JsonProperty
@JsonDeserialize(using = MembersDeserializer.class)
@JsonSerialize(using = MembersSerializer.class)
public Set<SignalServiceAddress> members = new HashSet<>();
@JsonProperty
public Set<RecipientId> members = new HashSet<>();
public String color;
@JsonProperty(defaultValue = "0")
public int messageExpirationTime;
@JsonProperty(defaultValue = "false")
public boolean blocked;
@JsonProperty
public Integer inboxPosition;
@JsonProperty(defaultValue = "false")
public boolean archived;
public GroupInfoV1(GroupIdV1 groupId) {
@ -57,41 +29,30 @@ public class GroupInfoV1 extends GroupInfo {
}
public GroupInfoV1(
@JsonProperty("groupId") byte[] groupId,
@JsonProperty("expectedV2Id") byte[] expectedV2Id,
@JsonProperty("name") String name,
@JsonProperty("members") Collection<SignalServiceAddress> members,
@JsonProperty("avatarId") long _ignored_avatarId,
@JsonProperty("color") String color,
@JsonProperty("blocked") boolean blocked,
@JsonProperty("inboxPosition") Integer inboxPosition,
@JsonProperty("archived") boolean archived,
@JsonProperty("messageExpirationTime") int messageExpirationTime,
@JsonProperty("active") boolean _ignored_active
final GroupIdV1 groupId,
final GroupIdV2 expectedV2Id,
final String name,
final Set<RecipientId> members,
final String color,
final int messageExpirationTime,
final boolean blocked,
final boolean archived
) {
this.groupId = GroupId.v1(groupId);
this.expectedV2Id = GroupId.v2(expectedV2Id);
this.groupId = groupId;
this.expectedV2Id = expectedV2Id;
this.name = name;
this.members.addAll(members);
this.members = members;
this.color = color;
this.blocked = blocked;
this.inboxPosition = inboxPosition;
this.archived = archived;
this.messageExpirationTime = messageExpirationTime;
this.blocked = blocked;
this.archived = archived;
}
@Override
@JsonIgnore
public GroupIdV1 getGroupId() {
return groupId;
}
@JsonProperty("groupId")
private byte[] getGroupIdJackson() {
return groupId.serialize();
}
@JsonIgnore
public GroupIdV2 getExpectedV2Id() {
if (expectedV2Id == null) {
expectedV2Id = GroupUtils.getGroupIdV2(groupId);
@ -99,11 +60,6 @@ public class GroupInfoV1 extends GroupInfo {
return expectedV2Id;
}
@JsonProperty("expectedV2Id")
private byte[] getExpectedV2IdJackson() {
return getExpectedV2Id().serialize();
}
@Override
public String getTitle() {
return name;
@ -114,8 +70,7 @@ public class GroupInfoV1 extends GroupInfo {
return null;
}
@JsonIgnore
public Set<SignalServiceAddress> getMembers() {
public Set<RecipientId> getMembers() {
return members;
}
@ -134,79 +89,11 @@ public class GroupInfoV1 extends GroupInfo {
return messageExpirationTime;
}
public void addMembers(Collection<SignalServiceAddress> addresses) {
for (var address : addresses) {
if (this.members.contains(address)) {
continue;
}
removeMember(address);
this.members.add(address);
}
public void addMembers(Collection<RecipientId> members) {
this.members.addAll(members);
}
public void removeMember(SignalServiceAddress address) {
this.members.removeIf(member -> member.matches(address));
}
private static final class JsonSignalServiceAddress {
@JsonProperty
private UUID uuid;
@JsonProperty
private String number;
JsonSignalServiceAddress(@JsonProperty("uuid") final UUID uuid, @JsonProperty("number") final String number) {
this.uuid = uuid;
this.number = number;
}
JsonSignalServiceAddress(SignalServiceAddress address) {
this.uuid = address.getUuid().orNull();
this.number = address.getNumber().orNull();
}
SignalServiceAddress toSignalServiceAddress() {
return new SignalServiceAddress(uuid, number);
}
}
private static class MembersSerializer extends JsonSerializer<Set<SignalServiceAddress>> {
@Override
public void serialize(
final Set<SignalServiceAddress> value, final JsonGenerator jgen, final SerializerProvider provider
) throws IOException {
jgen.writeStartArray(value.size());
for (var address : value) {
if (address.getUuid().isPresent()) {
jgen.writeObject(new JsonSignalServiceAddress(address));
} else {
jgen.writeString(address.getNumber().get());
}
}
jgen.writeEndArray();
}
}
private static class MembersDeserializer extends JsonDeserializer<Set<SignalServiceAddress>> {
@Override
public Set<SignalServiceAddress> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
var addresses = new HashSet<SignalServiceAddress>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (var n : node) {
if (n.isTextual()) {
addresses.add(new SignalServiceAddress(null, n.textValue()));
} else {
var address = jsonProcessor.treeToValue(n, JsonSignalServiceAddress.class);
addresses.add(address.toSignalServiceAddress());
}
}
return addresses;
}
public void removeMember(RecipientId recipientId) {
this.members.removeIf(member -> member.equals(recipientId));
}
}

View file

@ -2,6 +2,8 @@ package org.asamk.signal.manager.storage.groups;
import org.asamk.signal.manager.groups.GroupIdV2;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.groups.GroupMasterKey;
@ -18,12 +20,19 @@ public class GroupInfoV2 extends GroupInfo {
private boolean blocked;
private DecryptedGroup group; // stored as a file with hexadecimal groupId as name
private RecipientResolver recipientResolver;
public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) {
this.groupId = groupId;
this.masterKey = masterKey;
}
public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey, final boolean blocked) {
this.groupId = groupId;
this.masterKey = masterKey;
this.blocked = blocked;
}
@Override
public GroupIdV2 getGroupId() {
return groupId;
@ -33,8 +42,9 @@ public class GroupInfoV2 extends GroupInfo {
return masterKey;
}
public void setGroup(final DecryptedGroup group) {
public void setGroup(final DecryptedGroup group, final RecipientResolver recipientResolver) {
this.group = group;
this.recipientResolver = recipientResolver;
}
public DecryptedGroup getGroup() {
@ -63,35 +73,38 @@ public class GroupInfoV2 extends GroupInfo {
}
@Override
public Set<SignalServiceAddress> getMembers() {
public Set<RecipientId> getMembers() {
if (this.group == null) {
return Set.of();
}
return group.getMembersList()
.stream()
.map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
.map(recipientResolver::resolveRecipient)
.collect(Collectors.toSet());
}
@Override
public Set<SignalServiceAddress> getPendingMembers() {
public Set<RecipientId> getPendingMembers() {
if (this.group == null) {
return Set.of();
}
return group.getPendingMembersList()
.stream()
.map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
.map(recipientResolver::resolveRecipient)
.collect(Collectors.toSet());
}
@Override
public Set<SignalServiceAddress> getRequestingMembers() {
public Set<RecipientId> getRequestingMembers() {
if (this.group == null) {
return Set.of();
}
return group.getRequestingMembersList()
.stream()
.map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
.map(recipientResolver::resolveRecipient)
.collect(Collectors.toSet());
}

View file

@ -0,0 +1,474 @@
package org.asamk.signal.manager.storage.groups;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupIdV1;
import org.asamk.signal.manager.groups.GroupIdV2;
import org.asamk.signal.manager.groups.GroupUtils;
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.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.util.Hex;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class GroupStore {
private final static Logger logger = LoggerFactory.getLogger(GroupStore.class);
private final File groupCachePath;
private final Map<GroupId, GroupInfo> groups;
private final RecipientResolver recipientResolver;
private final Saver saver;
private GroupStore(
final File groupCachePath,
final Map<GroupId, GroupInfo> groups,
final RecipientResolver recipientResolver,
final Saver saver
) {
this.groupCachePath = groupCachePath;
this.groups = groups;
this.recipientResolver = recipientResolver;
this.saver = saver;
}
public GroupStore(
final File groupCachePath, final RecipientResolver recipientResolver, final Saver saver
) {
this.groups = new HashMap<>();
this.groupCachePath = groupCachePath;
this.recipientResolver = recipientResolver;
this.saver = saver;
}
public static GroupStore fromStorage(
final Storage storage,
final File groupCachePath,
final RecipientResolver recipientResolver,
final Saver saver
) {
final var groups = storage.groups.stream().map(g -> {
if (g instanceof Storage.GroupV1) {
final var g1 = (Storage.GroupV1) g;
final var members = g1.members.stream().map(m -> {
if (m.recipientId == null) {
return recipientResolver.resolveRecipient(new SignalServiceAddress(UuidUtil.parseOrNull(m.uuid),
m.number));
}
return RecipientId.of(m.recipientId);
}).collect(Collectors.toSet());
return new GroupInfoV1(GroupIdV1.fromBase64(g1.groupId),
g1.expectedV2Id == null ? null : GroupIdV2.fromBase64(g1.expectedV2Id),
g1.name,
members,
g1.color,
g1.messageExpirationTime,
g1.blocked,
g1.archived);
}
final var g2 = (Storage.GroupV2) g;
var groupId = GroupIdV2.fromBase64(g2.groupId);
GroupMasterKey masterKey;
try {
masterKey = new GroupMasterKey(Base64.getDecoder().decode(g2.masterKey));
} catch (InvalidInputException | IllegalArgumentException e) {
throw new AssertionError("Invalid master key for group " + groupId.toBase64());
}
return new GroupInfoV2(groupId, masterKey, g2.blocked);
}).collect(Collectors.toMap(GroupInfo::getGroupId, g -> g));
return new GroupStore(groupCachePath, groups, recipientResolver, saver);
}
public void updateGroup(GroupInfo group) {
final Storage storage;
synchronized (groups) {
groups.put(group.getGroupId(), group);
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) {
try {
IOUtils.createPrivateDirectories(groupCachePath);
try (var stream = new FileOutputStream(getGroupV2File(group.getGroupId()))) {
((GroupInfoV2) group).getGroup().writeTo(stream);
}
final var groupFileLegacy = getGroupV2FileLegacy(group.getGroupId());
if (groupFileLegacy.exists()) {
groupFileLegacy.delete();
}
} catch (IOException e) {
logger.warn("Failed to cache group, ignoring: {}", e.getMessage());
}
}
storage = toStorageLocked();
}
saver.save(storage);
}
public void deleteGroupV1(GroupIdV1 groupId) {
final Storage storage;
synchronized (groups) {
groups.remove(groupId);
storage = toStorageLocked();
}
saver.save(storage);
}
public GroupInfo getGroup(GroupId groupId) {
synchronized (groups) {
return getGroupLocked(groupId);
}
}
public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) {
synchronized (groups) {
var group = getGroupLocked(groupId);
if (group instanceof GroupInfoV1) {
return (GroupInfoV1) group;
}
if (group == null) {
return new GroupInfoV1(groupId);
}
return null;
}
}
public List<GroupInfo> getGroups() {
synchronized (groups) {
final var groups = this.groups.values();
for (var group : groups) {
loadDecryptedGroupLocked(group);
}
return new ArrayList<>(groups);
}
}
public void mergeRecipients(final RecipientId recipientId, final RecipientId toBeMergedRecipientId) {
synchronized (groups) {
var modified = false;
for (var group : this.groups.values()) {
if (group instanceof GroupInfoV1) {
var groupV1 = (GroupInfoV1) group;
if (groupV1.isMember(toBeMergedRecipientId)) {
groupV1.removeMember(toBeMergedRecipientId);
groupV1.addMembers(List.of(recipientId));
modified = true;
}
}
}
if (modified) {
saver.save(toStorageLocked());
}
}
}
private GroupInfo getGroupLocked(final GroupId groupId) {
var group = groups.get(groupId);
if (group == null) {
if (groupId instanceof GroupIdV1) {
group = getGroupByV1IdLocked((GroupIdV1) groupId);
} else if (groupId instanceof GroupIdV2) {
group = getGroupV1ByV2IdLocked((GroupIdV2) groupId);
}
}
loadDecryptedGroupLocked(group);
return group;
}
private GroupInfo getGroupByV1IdLocked(final GroupIdV1 groupId) {
return groups.get(GroupUtils.getGroupIdV2(groupId));
}
private GroupInfoV1 getGroupV1ByV2IdLocked(GroupIdV2 groupIdV2) {
for (var g : groups.values()) {
if (g instanceof GroupInfoV1) {
final var gv1 = (GroupInfoV1) g;
if (groupIdV2.equals(gv1.getExpectedV2Id())) {
return gv1;
}
}
}
return null;
}
private void loadDecryptedGroupLocked(final GroupInfo group) {
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
var groupFile = getGroupV2File(group.getGroupId());
if (!groupFile.exists()) {
groupFile = getGroupV2FileLegacy(group.getGroupId());
}
if (!groupFile.exists()) {
return;
}
try (var stream = new FileInputStream(groupFile)) {
((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream), recipientResolver);
} catch (IOException ignored) {
}
}
}
private File getGroupV2FileLegacy(final GroupId groupId) {
return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize()));
}
private File getGroupV2File(final GroupId groupId) {
return new File(groupCachePath, groupId.toBase64().replace("/", "_"));
}
private Storage toStorageLocked() {
return new Storage(groups.values().stream().map(g -> {
if (g instanceof GroupInfoV1) {
final var g1 = (GroupInfoV1) g;
return new Storage.GroupV1(g1.getGroupId().toBase64(),
g1.getExpectedV2Id().toBase64(),
g1.name,
g1.color,
g1.messageExpirationTime,
g1.blocked,
g1.archived,
g1.members.stream()
.map(m -> new Storage.GroupV1.Member(m.getId(), null, null))
.collect(Collectors.toList()));
}
final var g2 = (GroupInfoV2) g;
return new Storage.GroupV2(g2.getGroupId().toBase64(),
Base64.getEncoder().encodeToString(g2.getMasterKey().serialize()),
g2.isBlocked());
}).collect(Collectors.toList()));
}
public static class Storage {
// @JsonSerialize(using = GroupsSerializer.class)
@JsonDeserialize(using = GroupsDeserializer.class)
public List<Storage.Group> groups;
// For deserialization
public Storage() {
}
public Storage(final List<Storage.Group> groups) {
this.groups = groups;
}
private abstract static class Group {
}
private static class GroupV1 extends Group {
public String groupId;
public String expectedV2Id;
public String name;
public String color;
public int messageExpirationTime;
public boolean blocked;
public boolean archived;
@JsonDeserialize(using = MembersDeserializer.class)
@JsonSerialize(using = MembersSerializer.class)
public List<Member> members;
// For deserialization
public GroupV1() {
}
public GroupV1(
final String groupId,
final String expectedV2Id,
final String name,
final String color,
final int messageExpirationTime,
final boolean blocked,
final boolean archived,
final List<Member> members
) {
this.groupId = groupId;
this.expectedV2Id = expectedV2Id;
this.name = name;
this.color = color;
this.messageExpirationTime = messageExpirationTime;
this.blocked = blocked;
this.archived = archived;
this.members = members;
}
private static final class Member {
public Long recipientId;
public String uuid;
public String number;
Member(Long recipientId, final String uuid, final String number) {
this.recipientId = recipientId;
this.uuid = uuid;
this.number = number;
}
}
private static final class JsonSignalServiceAddress {
public String uuid;
public String number;
// For deserialization
public JsonSignalServiceAddress() {
}
JsonSignalServiceAddress(final String uuid, final String number) {
this.uuid = uuid;
this.number = number;
}
}
private static class MembersSerializer extends JsonSerializer<List<Member>> {
@Override
public void serialize(
final List<Member> value, final JsonGenerator jgen, final SerializerProvider provider
) throws IOException {
jgen.writeStartArray(value.size());
for (var address : value) {
if (address.recipientId != null) {
jgen.writeNumber(address.recipientId);
} else if (address.uuid != null) {
jgen.writeObject(new JsonSignalServiceAddress(address.uuid, address.number));
} else {
jgen.writeString(address.number);
}
}
jgen.writeEndArray();
}
}
private static class MembersDeserializer extends JsonDeserializer<List<Member>> {
@Override
public List<Member> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
var addresses = new ArrayList<Member>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (var n : node) {
if (n.isTextual()) {
addresses.add(new Member(null, null, n.textValue()));
} else if (n.isNumber()) {
addresses.add(new Member(n.numberValue().longValue(), null, null));
} else {
var address = jsonParser.getCodec().treeToValue(n, JsonSignalServiceAddress.class);
addresses.add(new Member(null, address.uuid, address.number));
}
}
return addresses;
}
}
}
private static class GroupV2 extends Group {
public String groupId;
public String masterKey;
public boolean blocked;
// For deserialization
private GroupV2() {
}
public GroupV2(final String groupId, final String masterKey, final boolean blocked) {
this.groupId = groupId;
this.masterKey = masterKey;
this.blocked = blocked;
}
}
}
// private static class GroupsSerializer extends JsonSerializer<List<Storage.Group>> {
//
// @Override
// public void serialize(
// final List<Storage.Group> groups, final JsonGenerator jgen, final SerializerProvider provider
// ) throws IOException {
// jgen.writeStartArray(groups.size());
// for (var group : groups) {
// if (group instanceof GroupInfoV1) {
// jgen.writeObject(group);
// } else if (group instanceof GroupInfoV2) {
// final var groupV2 = (GroupInfoV2) group;
// jgen.writeStartObject();
// jgen.writeStringField("groupId", groupV2.getGroupId().toBase64());
// jgen.writeStringField("masterKey",
// Base64.getEncoder().encodeToString(groupV2.getMasterKey().serialize()));
// jgen.writeBooleanField("blocked", groupV2.isBlocked());
// jgen.writeEndObject();
// } else {
// throw new AssertionError("Unknown group version");
// }
// }
// jgen.writeEndArray();
// }
// }
//
private static class GroupsDeserializer extends JsonDeserializer<List<Storage.Group>> {
@Override
public List<Storage.Group> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
var groups = new ArrayList<Storage.Group>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (var n : node) {
Storage.Group g;
if (n.hasNonNull("masterKey")) {
// a v2 group
g = jsonParser.getCodec().treeToValue(n, Storage.GroupV2.class);
} else {
g = jsonParser.getCodec().treeToValue(n, Storage.GroupV1.class);
}
groups.add(g);
}
return groups;
}
}
public interface Saver {
void save(Storage storage);
}
}

View file

@ -1,207 +0,0 @@
package org.asamk.signal.manager.storage.groups;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupIdV1;
import org.asamk.signal.manager.groups.GroupIdV2;
import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.util.IOUtils;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.util.Hex;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class JsonGroupStore {
private final static Logger logger = LoggerFactory.getLogger(JsonGroupStore.class);
private static final ObjectMapper jsonProcessor = new ObjectMapper();
@JsonIgnore
public File groupCachePath;
@JsonProperty("groups")
@JsonSerialize(using = GroupsSerializer.class)
@JsonDeserialize(using = GroupsDeserializer.class)
private final Map<GroupId, GroupInfo> groups = new HashMap<>();
private JsonGroupStore() {
}
public JsonGroupStore(final File groupCachePath) {
this.groupCachePath = groupCachePath;
}
public void updateGroup(GroupInfo group) {
groups.put(group.getGroupId(), group);
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) {
try {
IOUtils.createPrivateDirectories(groupCachePath);
try (var stream = new FileOutputStream(getGroupFile(group.getGroupId()))) {
((GroupInfoV2) group).getGroup().writeTo(stream);
}
final var groupFileLegacy = getGroupFileLegacy(group.getGroupId());
if (groupFileLegacy.exists()) {
groupFileLegacy.delete();
}
} catch (IOException e) {
logger.warn("Failed to cache group, ignoring: {}", e.getMessage());
}
}
}
public void deleteGroup(GroupId groupId) {
groups.remove(groupId);
}
public GroupInfo getGroup(GroupId groupId) {
var group = groups.get(groupId);
if (group == null) {
if (groupId instanceof GroupIdV1) {
group = groups.get(GroupUtils.getGroupIdV2((GroupIdV1) groupId));
} else if (groupId instanceof GroupIdV2) {
group = getGroupV1ByV2Id((GroupIdV2) groupId);
}
}
loadDecryptedGroup(group);
return group;
}
private GroupInfoV1 getGroupV1ByV2Id(GroupIdV2 groupIdV2) {
for (var g : groups.values()) {
if (g instanceof GroupInfoV1) {
final var gv1 = (GroupInfoV1) g;
if (groupIdV2.equals(gv1.getExpectedV2Id())) {
return gv1;
}
}
}
return null;
}
private void loadDecryptedGroup(final GroupInfo group) {
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
var groupFile = getGroupFile(group.getGroupId());
if (!groupFile.exists()) {
groupFile = getGroupFileLegacy(group.getGroupId());
}
if (!groupFile.exists()) {
return;
}
try (var stream = new FileInputStream(groupFile)) {
((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream));
} catch (IOException ignored) {
}
}
}
private File getGroupFileLegacy(final GroupId groupId) {
return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize()));
}
private File getGroupFile(final GroupId groupId) {
return new File(groupCachePath, groupId.toBase64().replace("/", "_"));
}
public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) {
var group = getGroup(groupId);
if (group instanceof GroupInfoV1) {
return (GroupInfoV1) group;
}
if (group == null) {
return new GroupInfoV1(groupId);
}
return null;
}
@JsonIgnore
public List<GroupInfo> getGroups() {
final var groups = this.groups.values();
for (var group : groups) {
loadDecryptedGroup(group);
}
return new ArrayList<>(groups);
}
private static class GroupsSerializer extends JsonSerializer<Map<String, GroupInfo>> {
@Override
public void serialize(
final Map<String, GroupInfo> value, final JsonGenerator jgen, final SerializerProvider provider
) throws IOException {
final var groups = value.values();
jgen.writeStartArray(groups.size());
for (var group : groups) {
if (group instanceof GroupInfoV1) {
jgen.writeObject(group);
} else if (group instanceof GroupInfoV2) {
final var groupV2 = (GroupInfoV2) group;
jgen.writeStartObject();
jgen.writeStringField("groupId", groupV2.getGroupId().toBase64());
jgen.writeStringField("masterKey",
Base64.getEncoder().encodeToString(groupV2.getMasterKey().serialize()));
jgen.writeBooleanField("blocked", groupV2.isBlocked());
jgen.writeEndObject();
} else {
throw new AssertionError("Unknown group version");
}
}
jgen.writeEndArray();
}
}
private static class GroupsDeserializer extends JsonDeserializer<Map<GroupId, GroupInfo>> {
@Override
public Map<GroupId, GroupInfo> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
var groups = new HashMap<GroupId, GroupInfo>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (var n : node) {
GroupInfo g;
if (n.hasNonNull("masterKey")) {
// a v2 group
var groupId = GroupIdV2.fromBase64(n.get("groupId").asText());
try {
var masterKey = new GroupMasterKey(Base64.getDecoder().decode(n.get("masterKey").asText()));
g = new GroupInfoV2(groupId, masterKey);
} catch (InvalidInputException | IllegalArgumentException e) {
throw new AssertionError("Invalid master key for group " + groupId.toBase64());
}
g.setBlocked(n.get("blocked").asBoolean(false));
} else {
g = jsonProcessor.treeToValue(n, GroupInfoV1.class);
}
groups.put(g.getGroupId(), g);
}
return groups;
}
}
}