]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java
ec5c5a70495c6e2a9f8c3f725c0acb1e8504222f
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / storage / senderKeys / SenderKeyRecordStore.java
1 package org.asamk.signal.manager.storage.senderKeys;
2
3 import org.asamk.signal.manager.storage.recipients.RecipientId;
4 import org.asamk.signal.manager.storage.recipients.RecipientResolver;
5 import org.asamk.signal.manager.util.IOUtils;
6 import org.slf4j.Logger;
7 import org.slf4j.LoggerFactory;
8 import org.whispersystems.libsignal.SignalProtocolAddress;
9 import org.whispersystems.libsignal.groups.state.SenderKeyRecord;
10
11 import java.io.File;
12 import java.io.FileInputStream;
13 import java.io.FileOutputStream;
14 import java.io.IOException;
15 import java.nio.file.Files;
16 import java.util.Arrays;
17 import java.util.HashMap;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Objects;
21 import java.util.UUID;
22 import java.util.regex.Matcher;
23 import java.util.regex.Pattern;
24
25 public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups.state.SenderKeyStore {
26
27 private final static Logger logger = LoggerFactory.getLogger(SenderKeyRecordStore.class);
28
29 private final Map<Key, SenderKeyRecord> cachedSenderKeys = new HashMap<>();
30
31 private final File senderKeysPath;
32
33 private final RecipientResolver resolver;
34
35 public SenderKeyRecordStore(
36 final File senderKeysPath, final RecipientResolver resolver
37 ) {
38 this.senderKeysPath = senderKeysPath;
39 this.resolver = resolver;
40 }
41
42 @Override
43 public SenderKeyRecord loadSenderKey(final SignalProtocolAddress address, final UUID distributionId) {
44 final var key = getKey(address, distributionId);
45
46 synchronized (cachedSenderKeys) {
47 return loadSenderKeyLocked(key);
48 }
49 }
50
51 @Override
52 public void storeSenderKey(
53 final SignalProtocolAddress address, final UUID distributionId, final SenderKeyRecord record
54 ) {
55 final var key = getKey(address, distributionId);
56
57 synchronized (cachedSenderKeys) {
58 storeSenderKeyLocked(key, record);
59 }
60 }
61
62 long getCreateTimeForKey(final RecipientId selfRecipientId, final int selfDeviceId, final UUID distributionId) {
63 final var key = getKey(selfRecipientId, selfDeviceId, distributionId);
64 final var senderKeyFile = getSenderKeyFile(key);
65
66 if (!senderKeyFile.exists()) {
67 return -1;
68 }
69
70 return IOUtils.getFileCreateTime(senderKeyFile);
71 }
72
73 void deleteSenderKey(final RecipientId recipientId, final UUID distributionId) {
74 synchronized (cachedSenderKeys) {
75 cachedSenderKeys.clear();
76 final var keys = getKeysLocked(recipientId);
77 for (var key : keys) {
78 if (key.distributionId.equals(distributionId)) {
79 deleteSenderKeyLocked(key);
80 }
81 }
82 }
83 }
84
85 void deleteAll() {
86 synchronized (cachedSenderKeys) {
87 cachedSenderKeys.clear();
88 final var files = senderKeysPath.listFiles((_file, s) -> senderKeyFileNamePattern.matcher(s).matches());
89 if (files == null) {
90 return;
91 }
92
93 for (final var file : files) {
94 try {
95 Files.delete(file.toPath());
96 } catch (IOException e) {
97 logger.error("Failed to delete sender key file {}: {}", file, e.getMessage());
98 }
99 }
100 }
101 }
102
103 void deleteAllFor(final RecipientId recipientId) {
104 synchronized (cachedSenderKeys) {
105 cachedSenderKeys.clear();
106 final var keys = getKeysLocked(recipientId);
107 for (var key : keys) {
108 deleteSenderKeyLocked(key);
109 }
110 }
111 }
112
113 void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
114 synchronized (cachedSenderKeys) {
115 final var keys = getKeysLocked(toBeMergedRecipientId);
116 final var otherHasSenderKeys = keys.size() > 0;
117 if (!otherHasSenderKeys) {
118 return;
119 }
120
121 logger.debug("To be merged recipient had sender keys, re-assigning to the new recipient.");
122 for (var key : keys) {
123 final var toBeMergedSenderKey = loadSenderKeyLocked(key);
124 deleteSenderKeyLocked(key);
125 if (toBeMergedSenderKey == null) {
126 continue;
127 }
128
129 final var newKey = new Key(recipientId, key.deviceId(), key.distributionId);
130 final var senderKeyRecord = loadSenderKeyLocked(newKey);
131 if (senderKeyRecord != null) {
132 continue;
133 }
134 storeSenderKeyLocked(newKey, toBeMergedSenderKey);
135 }
136 }
137 }
138
139 /**
140 * @param identifier can be either a serialized uuid or a e164 phone number
141 */
142 private RecipientId resolveRecipient(String identifier) {
143 return resolver.resolveRecipient(identifier);
144 }
145
146 private Key getKey(final RecipientId recipientId, int deviceId, final UUID distributionId) {
147 return new Key(recipientId, deviceId, distributionId);
148 }
149
150 private Key getKey(final SignalProtocolAddress address, final UUID distributionId) {
151 final var recipientId = resolveRecipient(address.getName());
152 return new Key(recipientId, address.getDeviceId(), distributionId);
153 }
154
155 private List<Key> getKeysLocked(RecipientId recipientId) {
156 final var files = senderKeysPath.listFiles((_file, s) -> s.startsWith(recipientId.id() + "_"));
157 if (files == null) {
158 return List.of();
159 }
160 return parseFileNames(files);
161 }
162
163 final Pattern senderKeyFileNamePattern = Pattern.compile("([0-9]+)_([0-9]+)_([0-9a-z\\-]+)");
164
165 private List<Key> parseFileNames(final File[] files) {
166 return Arrays.stream(files)
167 .map(f -> senderKeyFileNamePattern.matcher(f.getName()))
168 .filter(Matcher::matches)
169 .map(matcher -> {
170 final var recipientId = resolver.resolveRecipient(Long.parseLong(matcher.group(1)));
171 if (recipientId == null) {
172 return null;
173 }
174 return new Key(recipientId, Integer.parseInt(matcher.group(2)), UUID.fromString(matcher.group(3)));
175 })
176 .filter(Objects::nonNull)
177 .toList();
178 }
179
180 private File getSenderKeyFile(Key key) {
181 try {
182 IOUtils.createPrivateDirectories(senderKeysPath);
183 } catch (IOException e) {
184 throw new AssertionError("Failed to create sender keys path: " + e.getMessage(), e);
185 }
186 return new File(senderKeysPath,
187 key.recipientId().id() + "_" + key.deviceId() + "_" + key.distributionId.toString());
188 }
189
190 private SenderKeyRecord loadSenderKeyLocked(final Key key) {
191 {
192 final var senderKeyRecord = cachedSenderKeys.get(key);
193 if (senderKeyRecord != null) {
194 return senderKeyRecord;
195 }
196 }
197
198 final var file = getSenderKeyFile(key);
199 if (!file.exists()) {
200 return null;
201 }
202 try (var inputStream = new FileInputStream(file)) {
203 final var senderKeyRecord = new SenderKeyRecord(inputStream.readAllBytes());
204 cachedSenderKeys.put(key, senderKeyRecord);
205 return senderKeyRecord;
206 } catch (IOException e) {
207 logger.warn("Failed to load sender key, resetting sender key: {}", e.getMessage());
208 return null;
209 }
210 }
211
212 private void storeSenderKeyLocked(final Key key, final SenderKeyRecord senderKeyRecord) {
213 cachedSenderKeys.put(key, senderKeyRecord);
214
215 final var file = getSenderKeyFile(key);
216 try {
217 try (var outputStream = new FileOutputStream(file)) {
218 outputStream.write(senderKeyRecord.serialize());
219 }
220 } catch (IOException e) {
221 logger.warn("Failed to store sender key, trying to delete file and retry: {}", e.getMessage());
222 try {
223 Files.delete(file.toPath());
224 try (var outputStream = new FileOutputStream(file)) {
225 outputStream.write(senderKeyRecord.serialize());
226 }
227 } catch (IOException e2) {
228 logger.error("Failed to store sender key file {}: {}", file, e2.getMessage());
229 }
230 }
231 }
232
233 private void deleteSenderKeyLocked(final Key key) {
234 cachedSenderKeys.remove(key);
235
236 final var file = getSenderKeyFile(key);
237 if (!file.exists()) {
238 return;
239 }
240 try {
241 Files.delete(file.toPath());
242 } catch (IOException e) {
243 logger.error("Failed to delete sender key file {}: {}", file, e.getMessage());
244 }
245 }
246
247 private record Key(RecipientId recipientId, int deviceId, UUID distributionId) {}
248 }