]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java
Reformat files
[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
88 return capabilities;
89 }
90
91 private static String decryptString(
92 final String encrypted,
93 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,
104 final ProfileCipher profileCipher
105 ) throws InvalidCiphertextException {
106 try {
107 return encrypted == null
108 ? Optional.empty()
109 : profileCipher.decryptBoolean(Base64.getDecoder().decode(encrypted));
110 } catch (IllegalArgumentException e) {
111 return Optional.empty();
112 }
113 }
114
115 private static byte[] decryptAndVerifyMobileCoinAddress(
116 final byte[] encryptedPaymentAddress,
117 final ProfileCipher profileCipher,
118 final ECPublicKey publicKey
119 ) throws InvalidCiphertextException {
120 byte[] decrypted;
121 try {
122 decrypted = profileCipher.decryptWithLength(encryptedPaymentAddress);
123 } catch (IOException e) {
124 logger.debug("Failed to decrypt payment address", e);
125 return null;
126 }
127
128 PaymentAddress paymentAddress;
129 try {
130 paymentAddress = PaymentAddress.ADAPTER.decode(decrypted);
131 } catch (IOException e) {
132 logger.debug("Failed to parse payment address", e);
133 return null;
134 }
135
136 return PaymentUtils.verifyPaymentsAddress(paymentAddress, publicKey);
137 }
138
139 private static Pair<String, String> splitName(String name) {
140 if (name == null) {
141 return new Pair<>(null, null);
142 }
143 String[] parts = name.split("\0");
144
145 return switch (parts.length) {
146 case 0 -> new Pair<>(null, null);
147 case 1 -> new Pair<>(parts[0], null);
148 default -> new Pair<>(parts[0], parts[1]);
149 };
150 }
151 }