]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/identities/LegacyIdentityKeyStore.java
669602e52848418ffd975dfaf97b02ce693c417c
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / storage / identities / LegacyIdentityKeyStore.java
1 package org.asamk.signal.manager.storage.identities;
2
3 import com.fasterxml.jackson.databind.ObjectMapper;
4
5 import org.asamk.signal.manager.api.TrustLevel;
6 import org.asamk.signal.manager.storage.recipients.RecipientId;
7 import org.asamk.signal.manager.storage.recipients.RecipientResolver;
8 import org.asamk.signal.manager.util.IOUtils;
9 import org.signal.libsignal.protocol.IdentityKey;
10 import org.signal.libsignal.protocol.InvalidKeyException;
11 import org.slf4j.Logger;
12 import org.slf4j.LoggerFactory;
13
14 import java.io.File;
15 import java.io.FileInputStream;
16 import java.io.IOException;
17 import java.nio.file.Files;
18 import java.util.Arrays;
19 import java.util.Base64;
20 import java.util.List;
21 import java.util.Objects;
22 import java.util.regex.Pattern;
23
24 public class LegacyIdentityKeyStore {
25
26 private final static Logger logger = LoggerFactory.getLogger(LegacyIdentityKeyStore.class);
27 private static final ObjectMapper objectMapper = org.asamk.signal.manager.storage.Utils.createStorageObjectMapper();
28
29 public static void migrate(
30 final File identitiesPath, final RecipientResolver resolver, final IdentityKeyStore identityKeyStore
31 ) {
32 final var identities = getIdentities(identitiesPath, resolver);
33 identityKeyStore.addLegacyIdentities(identities);
34 removeIdentityFiles(identitiesPath);
35 }
36
37 static final Pattern identityFileNamePattern = Pattern.compile("(\\d+)");
38
39 private static List<IdentityInfo> getIdentities(final File identitiesPath, final RecipientResolver resolver) {
40 final var files = identitiesPath.listFiles();
41 if (files == null) {
42 return List.of();
43 }
44 return Arrays.stream(files)
45 .filter(f -> identityFileNamePattern.matcher(f.getName()).matches())
46 .map(f -> resolver.resolveRecipient(Long.parseLong(f.getName())))
47 .filter(Objects::nonNull)
48 .map(recipientId -> loadIdentityLocked(recipientId, identitiesPath))
49 .filter(Objects::nonNull)
50 .toList();
51 }
52
53 private static File getIdentityFile(final RecipientId recipientId, final File identitiesPath) {
54 try {
55 IOUtils.createPrivateDirectories(identitiesPath);
56 } catch (IOException e) {
57 throw new AssertionError("Failed to create identities path", e);
58 }
59 return new File(identitiesPath, String.valueOf(recipientId.id()));
60 }
61
62 private static IdentityInfo loadIdentityLocked(final RecipientId recipientId, final File identitiesPath) {
63 final var file = getIdentityFile(recipientId, identitiesPath);
64 if (!file.exists()) {
65 return null;
66 }
67 try (var inputStream = new FileInputStream(file)) {
68 var storage = objectMapper.readValue(inputStream, IdentityStorage.class);
69
70 var id = new IdentityKey(Base64.getDecoder().decode(storage.identityKey()));
71 var trustLevel = TrustLevel.fromInt(storage.trustLevel());
72 var added = storage.addedTimestamp();
73
74 return new IdentityInfo(recipientId, id, trustLevel, added);
75 } catch (IOException | InvalidKeyException e) {
76 logger.warn("Failed to load identity key: {}", e.getMessage());
77 return null;
78 }
79 }
80
81 private static void removeIdentityFiles(File identitiesPath) {
82 final var files = identitiesPath.listFiles();
83 if (files == null) {
84 return;
85 }
86
87 for (var file : files) {
88 try {
89 Files.delete(file.toPath());
90 } catch (IOException e) {
91 logger.error("Failed to delete identity file {}: {}", file, e.getMessage());
92 }
93 }
94 try {
95 Files.delete(identitiesPath.toPath());
96 } catch (IOException e) {
97 logger.error("Failed to delete identity directory {}: {}", identitiesPath, e.getMessage());
98 }
99 }
100
101 private record IdentityStorage(String identityKey, int trustLevel, long addedTimestamp) {}
102 }