1 /*
2  * Copyright (C) 2021 Finn Herzfeld
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 package io.finn.signald.clientprotocol.v1;
19 
20 import com.fasterxml.jackson.annotation.JsonProperty;
21 import com.google.protobuf.ByteString;
22 import io.finn.signald.Account;
23 import io.finn.signald.Empty;
24 import io.finn.signald.Manager;
25 import io.finn.signald.annotations.Doc;
26 import io.finn.signald.annotations.ExampleValue;
27 import io.finn.signald.annotations.ProtocolType;
28 import io.finn.signald.annotations.Required;
29 import io.finn.signald.clientprotocol.Request;
30 import io.finn.signald.clientprotocol.RequestType;
31 import io.finn.signald.clientprotocol.v1.exceptions.*;
32 import io.finn.signald.clientprotocol.v1.exceptions.InternalError;
33 import io.finn.signald.util.AttachmentUtil;
34 import java.io.File;
35 import java.io.IOException;
36 import org.signal.zkgroup.InvalidInputException;
37 import org.whispersystems.libsignal.IdentityKeyPair;
38 import org.whispersystems.libsignal.util.guava.Optional;
39 import org.whispersystems.signalservice.api.util.StreamDetails;
40 import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
41 import org.whispersystems.util.Base64;
42 
43 @ProtocolType("set_profile")
44 public class SetProfile implements RequestType<Empty> {
45   @ExampleValue(ExampleValue.LOCAL_PHONE_NUMBER) @Doc("The phone number of the account to use") @Required public String account;
46 
47   @ExampleValue("\"signald user\"") @Doc("New profile name. Set to empty string for no profile name") @Required public String name;
48 
49   @ExampleValue(ExampleValue.LOCAL_EXTERNAL_JPG) @Doc("Path to new profile avatar file. If unset or null, unset the profile avatar") public String avatarFile;
50 
51   @Doc("an optional about string. If unset, null or an empty string will unset profile about field") public String about;
52 
53   @Doc("an optional single emoji character. If unset, null or an empty string will unset profile emoji") public String emoji;
54 
55   @Doc("an optional *base64-encoded* MobileCoin address to set in the profile. Note that this is not the traditional "
56        + "MobileCoin address encoding, which is custom. Clients are responsible for converting between MobileCoin's "
57        + "custom base58 on the user-facing side and base64 encoding on the signald side. If unset, null or an empty "
58        + "string, will empty the profile payment address")
59   @JsonProperty("mobilecoin_address")
60   public String mobilecoinAddress;
61 
62   @Override
run(Request request)63   public Empty run(Request request) throws InternalError, InvalidProxyError, ServerNotFoundError, NoSuchAccountError, InvalidBase64Error {
64     File avatar = avatarFile == null ? null : new File(avatarFile);
65 
66     Manager m = Common.getManager(account);
67 
68     if (name == null) {
69       name = "";
70     }
71 
72     if (about == null) {
73       about = "";
74     }
75 
76     if (emoji == null) {
77       emoji = "";
78     }
79 
80     Optional<SignalServiceProtos.PaymentAddress> paymentAddress = Optional.absent();
81 
82     if (mobilecoinAddress != null && !mobilecoinAddress.equals("")) {
83       byte[] decodedAddress;
84       try {
85         decodedAddress = Base64.decode(mobilecoinAddress);
86       } catch (IOException e) {
87         throw new InvalidBase64Error();
88       }
89       Account a = Common.getAccount(account);
90       IdentityKeyPair identityKeyPair = a.getProtocolStore().getIdentityKeyPair();
91       SignalServiceProtos.PaymentAddress signedAddress = signPaymentsAddress(decodedAddress, identityKeyPair);
92 
93       SignalServiceProtos.PaymentAddress.Builder paymentAddressBuilder = SignalServiceProtos.PaymentAddress.newBuilder();
94       paymentAddressBuilder.setMobileCoinAddress(signedAddress.getMobileCoinAddress());
95 
96       paymentAddress = Optional.of(paymentAddressBuilder.build());
97     }
98 
99     try (final StreamDetails streamDetails = avatar == null ? null : AttachmentUtil.createStreamDetailsFromFile(avatar)) {
100       m.getAccountManager().setVersionedProfile(m.getUUID(), m.getAccountData().getProfileKey(), name, about, emoji, paymentAddress, streamDetails);
101     } catch (IOException e) {
102       throw new InternalError("error reading avatar file", e);
103     } catch (InvalidInputException e) {
104       throw new InternalError("error getting own profile key", e);
105     }
106 
107     return new Empty();
108   }
109 
signPaymentsAddress(byte[] publicAddressBytes, IdentityKeyPair identityKeyPair)110   static SignalServiceProtos.PaymentAddress signPaymentsAddress(byte[] publicAddressBytes, IdentityKeyPair identityKeyPair) {
111     byte[] signature = identityKeyPair.getPrivateKey().calculateSignature(publicAddressBytes);
112 
113     return SignalServiceProtos.PaymentAddress.newBuilder()
114         .setMobileCoinAddress(
115             SignalServiceProtos.PaymentAddress.MobileCoinAddress.newBuilder().setAddress(ByteString.copyFrom(publicAddressBytes)).setSignature(ByteString.copyFrom(signature)))
116         .build();
117   }
118 }
119