]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java
Implement username links
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / util / ProfileUtils.java
1 package org.asamk.signal.manager.util;
2
3 import org.asamk.signal.manager.api.Pair;
4 import org.asamk.signal.manager.api.Profile;
5 import org.signal.libsignal.protocol.IdentityKey;
6 import org.signal.libsignal.protocol.InvalidKeyException;
7 import org.signal.libsignal.protocol.ecc.ECPublicKey;
8 import org.signal.libsignal.zkgroup.profiles.ProfileKey;
9 import org.slf4j.Logger;
10 import org.slf4j.LoggerFactory;
11 import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
12 import org.whispersystems.signalservice.api.crypto.ProfileCipher;
13 import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
14 import org.whispersystems.signalservice.internal.push.PaymentAddress;
15
16 import java.io.IOException;
17 import java.util.Base64;
18 import java.util.HashSet;
19
20 public class ProfileUtils {
21
22 private static final Logger logger = LoggerFactory.getLogger(ProfileUtils.class);
23
24 public static Profile decryptProfile(
25 final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
26 ) {
27 var profileCipher = new ProfileCipher(profileKey);
28 IdentityKey identityKey = null;
29 try {
30 identityKey = new IdentityKey(Base64.getDecoder().decode(encryptedProfile.getIdentityKey()), 0);
31 } catch (InvalidKeyException e) {
32 logger.debug("Failed to decode identity key in profile, can't verify payment address", e);
33 }
34
35 try {
36 var name = decrypt(encryptedProfile.getName(), profileCipher);
37 var about = trimZeros(decrypt(encryptedProfile.getAbout(), profileCipher));
38 var aboutEmoji = trimZeros(decrypt(encryptedProfile.getAboutEmoji(), profileCipher));
39
40 final var nameParts = splitName(name);
41 return new Profile(System.currentTimeMillis(),
42 nameParts.first(),
43 nameParts.second(),
44 about,
45 aboutEmoji,
46 encryptedProfile.getAvatar(),
47 identityKey == null || encryptedProfile.getPaymentAddress() == null
48 ? null
49 : decryptAndVerifyMobileCoinAddress(encryptedProfile.getPaymentAddress(),
50 profileCipher,
51 identityKey.getPublicKey()),
52 getUnidentifiedAccessMode(encryptedProfile, profileCipher),
53 getCapabilities(encryptedProfile));
54 } catch (InvalidCiphertextException e) {
55 logger.debug("Failed to decrypt profile for {}", encryptedProfile.getServiceId(), e);
56 return null;
57 }
58 }
59
60 public static Profile.UnidentifiedAccessMode getUnidentifiedAccessMode(
61 final SignalServiceProfile encryptedProfile, final ProfileCipher profileCipher
62 ) {
63 if (encryptedProfile.isUnrestrictedUnidentifiedAccess()) {
64 return Profile.UnidentifiedAccessMode.UNRESTRICTED;
65 }
66
67 if (encryptedProfile.getUnidentifiedAccess() != null && profileCipher != null) {
68 final var unidentifiedAccessVerifier = Base64.getDecoder().decode(encryptedProfile.getUnidentifiedAccess());
69 if (profileCipher.verifyUnidentifiedAccess(unidentifiedAccessVerifier)) {
70 return Profile.UnidentifiedAccessMode.ENABLED;
71 }
72 }
73
74 return Profile.UnidentifiedAccessMode.DISABLED;
75 }
76
77 public static HashSet<Profile.Capability> getCapabilities(final SignalServiceProfile encryptedProfile) {
78 final var capabilities = new HashSet<Profile.Capability>();
79 if (encryptedProfile.getCapabilities().isGv1Migration()) {
80 capabilities.add(Profile.Capability.gv1Migration);
81 }
82 if (encryptedProfile.getCapabilities().isStorage()) {
83 capabilities.add(Profile.Capability.storage);
84 }
85 if (encryptedProfile.getCapabilities().isSenderKey()) {
86 capabilities.add(Profile.Capability.senderKey);
87 }
88 if (encryptedProfile.getCapabilities().isAnnouncementGroup()) {
89 capabilities.add(Profile.Capability.announcementGroup);
90 }
91
92 return capabilities;
93 }
94
95 private static String decrypt(
96 final String encryptedName, final ProfileCipher profileCipher
97 ) throws InvalidCiphertextException {
98 try {
99 return encryptedName == null
100 ? null
101 : new String(profileCipher.decrypt(Base64.getDecoder().decode(encryptedName)));
102 } catch (IllegalArgumentException e) {
103 return null;
104 }
105 }
106
107 private static byte[] decryptAndVerifyMobileCoinAddress(
108 final byte[] encryptedPaymentAddress, final ProfileCipher profileCipher, final ECPublicKey publicKey
109 ) throws InvalidCiphertextException {
110 byte[] decrypted;
111 try {
112 decrypted = profileCipher.decryptWithLength(encryptedPaymentAddress);
113 } catch (IOException e) {
114 logger.debug("Failed to decrypt payment address", e);
115 return null;
116 }
117
118 PaymentAddress paymentAddress;
119 try {
120 paymentAddress = PaymentAddress.ADAPTER.decode(decrypted);
121 } catch (IOException e) {
122 logger.debug("Failed to parse payment address", e);
123 return null;
124 }
125
126 return PaymentUtils.verifyPaymentsAddress(paymentAddress, publicKey);
127 }
128
129 private static Pair<String, String> splitName(String name) {
130 if (name == null) {
131 return new Pair<>(null, null);
132 }
133 String[] parts = name.split("\0");
134
135 return switch (parts.length) {
136 case 0 -> new Pair<>(null, null);
137 case 1 -> new Pair<>(parts[0], null);
138 default -> new Pair<>(parts[0], parts[1]);
139 };
140 }
141
142 static String trimZeros(String str) {
143 if (str == null) {
144 return null;
145 }
146
147 int pos = str.indexOf(0);
148 return pos == -1 ? str : str.substring(0, pos);
149 }
150 }