1 package org
.asamk
.signal
.manager
.util
;
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
;
17 import java
.io
.IOException
;
18 import java
.util
.Base64
;
19 import java
.util
.HashSet
;
20 import java
.util
.Optional
;
22 public class ProfileUtils
{
24 private static final Logger logger
= LoggerFactory
.getLogger(ProfileUtils
.class);
26 public static Profile
decryptProfile(final ProfileKey profileKey
, final SignalServiceProfile encryptedProfile
) {
27 var profileCipher
= new ProfileCipher(profileKey
);
28 IdentityKey identityKey
= null;
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
);
36 var name
= decryptString(encryptedProfile
.getName(), profileCipher
);
37 var about
= decryptString(encryptedProfile
.getAbout(), profileCipher
);
38 var aboutEmoji
= decryptString(encryptedProfile
.getAboutEmoji(), profileCipher
);
40 final var nameParts
= splitName(name
);
41 final var remotePhoneNumberSharing
= decryptBoolean(encryptedProfile
.getPhoneNumberSharing(),
42 profileCipher
).map(v
-> v ? PhoneNumberSharingMode
.EVERYBODY
: PhoneNumberSharingMode
.NOBODY
)
44 return new Profile(System
.currentTimeMillis(),
49 encryptedProfile
.getAvatar(),
50 identityKey
== null || encryptedProfile
.getPaymentAddress() == null
52 : decryptAndVerifyMobileCoinAddress(encryptedProfile
.getPaymentAddress(),
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
);
64 public static Profile
.UnidentifiedAccessMode
getUnidentifiedAccessMode(
65 final SignalServiceProfile encryptedProfile
,
66 final ProfileCipher profileCipher
68 if (encryptedProfile
.isUnrestrictedUnidentifiedAccess()) {
69 return Profile
.UnidentifiedAccessMode
.UNRESTRICTED
;
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
;
79 return Profile
.UnidentifiedAccessMode
.DISABLED
;
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
);
87 if (encryptedProfile
.getCapabilities().isStorageServiceEncryptionV2()) {
88 capabilities
.add(Profile
.Capability
.storageServiceEncryptionV2Capability
);
94 private static String
decryptString(
95 final String encrypted
,
96 final ProfileCipher profileCipher
97 ) throws InvalidCiphertextException
{
99 return encrypted
== null ?
null : profileCipher
.decryptString(Base64
.getDecoder().decode(encrypted
));
100 } catch (IllegalArgumentException e
) {
105 private static Optional
<Boolean
> decryptBoolean(
106 final String encrypted
,
107 final ProfileCipher profileCipher
108 ) throws InvalidCiphertextException
{
110 return encrypted
== null
112 : profileCipher
.decryptBoolean(Base64
.getDecoder().decode(encrypted
));
113 } catch (IllegalArgumentException e
) {
114 return Optional
.empty();
118 private static byte[] decryptAndVerifyMobileCoinAddress(
119 final byte[] encryptedPaymentAddress
,
120 final ProfileCipher profileCipher
,
121 final ECPublicKey publicKey
122 ) throws InvalidCiphertextException
{
125 decrypted
= profileCipher
.decryptWithLength(encryptedPaymentAddress
);
126 } catch (IOException e
) {
127 logger
.debug("Failed to decrypt payment address", e
);
131 PaymentAddress paymentAddress
;
133 paymentAddress
= PaymentAddress
.ADAPTER
.decode(decrypted
);
134 } catch (IOException e
) {
135 logger
.debug("Failed to parse payment address", e
);
139 return PaymentUtils
.verifyPaymentsAddress(paymentAddress
, publicKey
);
142 private static Pair
<String
, String
> splitName(String name
) {
144 return new Pair
<>(null, null);
146 String
[] parts
= name
.split("\0");
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]);