1 package org
.asamk
.signal
.manager
.storage
.groups
;
3 import com
.fasterxml
.jackson
.annotation
.JsonInclude
;
4 import com
.fasterxml
.jackson
.core
.JsonParser
;
5 import com
.fasterxml
.jackson
.databind
.DeserializationContext
;
6 import com
.fasterxml
.jackson
.databind
.JsonDeserializer
;
7 import com
.fasterxml
.jackson
.databind
.JsonNode
;
8 import com
.fasterxml
.jackson
.databind
.annotation
.JsonDeserialize
;
10 import org
.asamk
.signal
.manager
.api
.GroupId
;
11 import org
.asamk
.signal
.manager
.api
.GroupIdV1
;
12 import org
.asamk
.signal
.manager
.api
.GroupIdV2
;
13 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientAddress
;
14 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientResolver
;
15 import org
.signal
.libsignal
.zkgroup
.InvalidInputException
;
16 import org
.signal
.libsignal
.zkgroup
.groups
.GroupMasterKey
;
17 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroup
;
18 import org
.slf4j
.Logger
;
19 import org
.slf4j
.LoggerFactory
;
20 import org
.whispersystems
.signalservice
.api
.push
.DistributionId
;
21 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
22 import org
.whispersystems
.signalservice
.internal
.util
.Hex
;
25 import java
.io
.FileInputStream
;
26 import java
.io
.IOException
;
27 import java
.nio
.file
.Files
;
28 import java
.util
.ArrayList
;
29 import java
.util
.Base64
;
30 import java
.util
.List
;
31 import java
.util
.Objects
;
32 import java
.util
.stream
.Collectors
;
34 public class LegacyGroupStore
{
36 private static final Logger logger
= LoggerFactory
.getLogger(LegacyGroupStore
.class);
38 public static void migrate(
39 final Storage storage
,
40 final File groupCachePath
,
41 final RecipientResolver recipientResolver
,
42 final GroupStore groupStore
44 final var groups
= storage
.groups
.stream().map(g
-> {
45 if (g
instanceof Storage
.GroupV1 g1
) {
46 final var members
= g1
.members
.stream().map(m
-> {
47 if (m
.recipientId
== null) {
48 return recipientResolver
.resolveRecipient(new RecipientAddress(ServiceId
.ACI
.parseOrNull(m
.uuid
),
52 return recipientResolver
.resolveRecipient(m
.recipientId
);
53 }).filter(Objects
::nonNull
).collect(Collectors
.toSet());
55 return new GroupInfoV1(GroupIdV1
.fromBase64(g1
.groupId
),
56 g1
.expectedV2Id
== null ?
null : GroupIdV2
.fromBase64(g1
.expectedV2Id
),
60 g1
.messageExpirationTime
,
66 final var g2
= (Storage
.GroupV2
) g
;
67 var groupId
= GroupIdV2
.fromBase64(g2
.groupId
);
68 GroupMasterKey masterKey
;
70 masterKey
= new GroupMasterKey(Base64
.getDecoder().decode(g2
.masterKey
));
71 } catch (InvalidInputException
| IllegalArgumentException e
) {
72 throw new AssertionError("Invalid master key for group " + groupId
.toBase64());
75 return new GroupInfoV2(groupId
,
77 loadDecryptedGroupLocked(groupId
, groupCachePath
),
78 g2
.distributionId
== null ? DistributionId
.create() : DistributionId
.from(g2
.distributionId
),
86 groupStore
.addLegacyGroups(groups
);
87 removeGroupCache(groupCachePath
);
90 private static void removeGroupCache(File groupCachePath
) {
91 final var files
= groupCachePath
.listFiles();
96 for (var file
: files
) {
98 Files
.delete(file
.toPath());
99 } catch (IOException e
) {
100 logger
.error("Failed to delete group cache file {}: {}", file
, e
.getMessage());
104 Files
.delete(groupCachePath
.toPath());
105 } catch (IOException e
) {
106 logger
.error("Failed to delete group cache directory {}: {}", groupCachePath
, e
.getMessage());
110 private static DecryptedGroup
loadDecryptedGroupLocked(final GroupIdV2 groupIdV2
, final File groupCachePath
) {
111 var groupFile
= getGroupV2File(groupIdV2
, groupCachePath
);
112 if (!groupFile
.exists()) {
113 groupFile
= getGroupV2FileLegacy(groupIdV2
, groupCachePath
);
115 if (!groupFile
.exists()) {
118 try (var stream
= new FileInputStream(groupFile
)) {
119 return DecryptedGroup
.ADAPTER
.decode(stream
);
120 } catch (IOException ignored
) {
125 private static File
getGroupV2FileLegacy(final GroupId groupId
, final File groupCachePath
) {
126 return new File(groupCachePath
, Hex
.toStringCondensed(groupId
.serialize()));
129 private static File
getGroupV2File(final GroupId groupId
, final File groupCachePath
) {
130 return new File(groupCachePath
, groupId
.toBase64().replace("/", "_"));
133 public record Storage(@JsonDeserialize(using
= GroupsDeserializer
.class) List
<Record
> groups
) {
135 public record GroupV1(
140 int messageExpirationTime
,
143 @JsonDeserialize(using
= MembersDeserializer
.class) List
<Member
> members
146 public record Member(Long recipientId
, String uuid
, String number
) {}
148 public record JsonRecipientAddress(String uuid
, String number
) {}
150 private static class MembersDeserializer
extends JsonDeserializer
<List
<Member
>> {
153 public List
<Member
> deserialize(
154 JsonParser jsonParser
, DeserializationContext deserializationContext
155 ) throws IOException
{
156 var addresses
= new ArrayList
<Member
>();
157 JsonNode node
= jsonParser
.getCodec().readTree(jsonParser
);
160 addresses
.add(new Member(null, null, n
.textValue()));
161 } else if (n
.isNumber()) {
162 addresses
.add(new Member(n
.numberValue().longValue(), null, null));
164 var address
= jsonParser
.getCodec().treeToValue(n
, JsonRecipientAddress
.class);
165 addresses
.add(new Member(null, address
.uuid
, address
.number
));
174 public record GroupV2(
177 String distributionId
,
178 @JsonInclude(JsonInclude
.Include
.NON_DEFAULT
) boolean blocked
,
179 @JsonInclude(JsonInclude
.Include
.NON_DEFAULT
) boolean permissionDenied
183 private static class GroupsDeserializer
extends JsonDeserializer
<List
<Object
>> {
186 public List
<Object
> deserialize(
187 JsonParser jsonParser
, DeserializationContext deserializationContext
188 ) throws IOException
{
189 var groups
= new ArrayList
<>();
190 JsonNode node
= jsonParser
.getCodec().readTree(jsonParser
);
193 if (n
.hasNonNull("masterKey")) {
195 g
= jsonParser
.getCodec().treeToValue(n
, Storage
.GroupV2
.class);
197 g
= jsonParser
.getCodec().treeToValue(n
, Storage
.GroupV1
.class);