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