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