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