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