]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java
Decrypt and verify the profile payment address
[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 ignored) {
34 }
35
36 try {
37 var name = decrypt(encryptedProfile.getName(), profileCipher);
38 var about = trimZeros(decrypt(encryptedProfile.getAbout(), profileCipher));
39 var aboutEmoji = trimZeros(decrypt(encryptedProfile.getAboutEmoji(), profileCipher));
40
41 final var nameParts = splitName(name);
42 return new Profile(System.currentTimeMillis(),
43 nameParts.first(),
44 nameParts.second(),
45 about,
46 aboutEmoji,
47 encryptedProfile.getAvatar(),
48 identityKey == null || encryptedProfile.getPaymentAddress() == null
49 ? null
50 : decryptAndVerifyMobileCoinAddress(encryptedProfile.getPaymentAddress(),
51 profileCipher,
52 identityKey.getPublicKey()),
53 getUnidentifiedAccessMode(encryptedProfile, profileCipher),
54 getCapabilities(encryptedProfile));
55 } catch (InvalidCiphertextException e) {
56 logger.debug("Failed to decrypt profile for {}", encryptedProfile.getServiceId(), e);
57 return null;
58 }
59 }
60
61 public static Profile.UnidentifiedAccessMode getUnidentifiedAccessMode(
62 final SignalServiceProfile encryptedProfile, final ProfileCipher profileCipher
63 ) {
64 if (encryptedProfile.isUnrestrictedUnidentifiedAccess()) {
65 return Profile.UnidentifiedAccessMode.UNRESTRICTED;
66 }
67
68 if (encryptedProfile.getUnidentifiedAccess() != null && profileCipher != null) {
69 final var unidentifiedAccessVerifier = Base64.getDecoder().decode(encryptedProfile.getUnidentifiedAccess());
70 if (profileCipher.verifyUnidentifiedAccess(unidentifiedAccessVerifier)) {
71 return Profile.UnidentifiedAccessMode.ENABLED;
72 }
73 }
74
75 return Profile.UnidentifiedAccessMode.DISABLED;
76 }
77
78 public static HashSet<Profile.Capability> getCapabilities(final SignalServiceProfile encryptedProfile) {
79 final var capabilities = new HashSet<Profile.Capability>();
80 if (encryptedProfile.getCapabilities().isGv1Migration()) {
81 capabilities.add(Profile.Capability.gv1Migration);
82 }
83 if (encryptedProfile.getCapabilities().isStorage()) {
84 capabilities.add(Profile.Capability.storage);
85 }
86 if (encryptedProfile.getCapabilities().isSenderKey()) {
87 capabilities.add(Profile.Capability.senderKey);
88 }
89 if (encryptedProfile.getCapabilities().isAnnouncementGroup()) {
90 capabilities.add(Profile.Capability.announcementGroup);
91 }
92
93 return capabilities;
94 }
95
96 private static String decrypt(
97 final String encryptedName, final ProfileCipher profileCipher
98 ) throws InvalidCiphertextException {
99 try {
100 return encryptedName == null
101 ? null
102 : new String(profileCipher.decrypt(Base64.getDecoder().decode(encryptedName)));
103 } catch (IllegalArgumentException e) {
104 return null;
105 }
106 }
107
108 private static byte[] decryptAndVerifyMobileCoinAddress(
109 final byte[] encryptedPaymentAddress, final ProfileCipher profileCipher, final ECPublicKey publicKey
110 ) throws InvalidCiphertextException {
111 byte[] decrypted;
112 try {
113 decrypted = profileCipher.decryptWithLength(encryptedPaymentAddress);
114 } catch (IOException e) {
115 return null;
116 }
117
118 SignalServiceProtos.PaymentAddress paymentAddress;
119 try {
120 paymentAddress = SignalServiceProtos.PaymentAddress.parseFrom(decrypted);
121 } catch (InvalidProtocolBufferException e) {
122 return null;
123 }
124
125 return PaymentUtils.verifyPaymentsAddress(paymentAddress, publicKey);
126 }
127
128 private static Pair<String, String> splitName(String name) {
129 if (name == null) {
130 return new Pair<>(null, null);
131 }
132 String[] parts = name.split("\0");
133
134 return switch (parts.length) {
135 case 0 -> new Pair<>(null, null);
136 case 1 -> new Pair<>(parts[0], null);
137 default -> new Pair<>(parts[0], parts[1]);
138 };
139 }
140
141 static String trimZeros(String str) {
142 if (str == null) {
143 return null;
144 }
145
146 int pos = str.indexOf(0);
147 return pos == -1 ? str : str.substring(0, pos);
148 }
149 }