Commit 42bf51d4 authored by zeroleak's avatar zeroleak
Browse files

add externalDestination support

parent e50378f7
......@@ -9,6 +9,7 @@ import com.samourai.whirlpool.protocol.WhirlpoolProtocol;
import java.lang.invoke.MethodHandles;
import java.util.Arrays;
import javax.annotation.PreDestroy;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -167,8 +168,12 @@ public class Application implements ApplicationRunner {
}
private static String[] computeRestartArgs() {
String[] ignoreArgs =
new String[] {
"--" + ApplicationArgs.ARG_INIT, "--" + ApplicationArgs.ARG_SET_EXTERNAL_XPUB
};
return Arrays.stream(applicationArguments.getSourceArgs())
.filter(a -> !a.toLowerCase().equals("--" + ApplicationArgs.ARG_INIT))
.filter(a -> !ArrayUtils.contains(ignoreArgs, a.toLowerCase()))
.toArray(i -> new String[i]);
}
}
......@@ -18,21 +18,22 @@ public class ApplicationArgs {
private static final String ARG_DEBUG = "debug";
private static final String ARG_DEBUG_CLIENT = "debug-client";
private static final String ARG_LIST_POOLS = "list-pools";
public static final String ARG_LIST_POOLS = "list-pools";
private static final String ARG_SCODE = "scode";
private static final String ARG_CLIENTS = "clients";
private static final String ARG_CLIENT_DELAY = "client-delay";
private static final String ARG_TX0_DELAY = "tx0-delay";
private static final String ARG_TX0_MAX_OUTPUTS = "tx0-max-outputs";
private static final String ARG_AGGREGATE_POSTMIX = "aggregate-postmix";
public static final String ARG_AGGREGATE_POSTMIX = "aggregate-postmix";
private static final String ARG_AUTO_AGGREGATE_POSTMIX = "auto-aggregate-postmix";
private static final String ARG_AUTO_TX0 = "auto-tx0";
private static final String ARG_AUTO_MIX = "auto-mix";
private static final String ARG_LISTEN = "listen";
private static final String ARG_API_KEY = "api-key";
public static final String ARG_INIT = "init";
public static final String ARG_SET_EXTERNAL_XPUB = "set-external-xpub";
private static final String ARG_AUTHENTICATE = "authenticate";
private static final String ARG_DUMP_PAYLOAD = "dump-payload";
public static final String ARG_DUMP_PAYLOAD = "dump-payload";
private static final String ARG_RESYNC = "resync";
private ApplicationArguments args;
......@@ -125,6 +126,10 @@ public class ApplicationArgs {
return args.containsOption(ARG_INIT);
}
public boolean isSetExternalXpub() {
return args.containsOption(ARG_SET_EXTERNAL_XPUB);
}
public boolean isAuthenticate() {
return args.containsOption(ARG_AUTHENTICATE);
}
......
......@@ -27,7 +27,7 @@ public abstract class AbstractRestController {
if (!Strings.isEmpty(cliConfig.getApiKey())) {
String requestApiKey = httpHeaders.getFirst(CliApi.HEADER_API_KEY);
if (!cliConfig.getApiKey().equals(requestApiKey)) {
throw new NotifiableException("API key rejected");
throw new NotifiableException("API key rejected: " + requestApiKey);
}
}
}
......
......@@ -6,6 +6,7 @@ import com.samourai.stomp.client.IStompClientService;
import com.samourai.wallet.api.backend.BackendApi;
import com.samourai.wallet.api.backend.BackendServer;
import com.samourai.wallet.util.FormatsUtilGeneric;
import com.samourai.whirlpool.client.exception.NotifiableException;
import com.samourai.whirlpool.client.utils.ClientUtils;
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig;
import java.util.Collection;
......@@ -29,7 +30,9 @@ public class CliConfig extends CliConfigFile {
public WhirlpoolWalletConfig computeWhirlpoolWalletConfig(
IHttpClientService httpClientService,
IStompClientService stompClientService,
BackendApi backendApi) {
BackendApi backendApi,
String passphrase)
throws NotifiableException {
// check valid
if (autoAggregatePostmix && StringUtils.isEmpty(autoTx0PoolId)) {
......@@ -37,7 +40,8 @@ public class CliConfig extends CliConfigFile {
}
WhirlpoolWalletConfig config =
super.computeWhirlpoolWalletConfig(httpClientService, stompClientService, backendApi);
super.computeWhirlpoolWalletConfig(
httpClientService, stompClientService, backendApi, passphrase);
config.setAutoTx0PoolId(autoTx0PoolId);
return config;
}
......
......@@ -3,11 +3,15 @@ package com.samourai.whirlpool.cli.config;
import com.samourai.http.client.IHttpClientService;
import com.samourai.stomp.client.IStompClientService;
import com.samourai.wallet.api.backend.BackendApi;
import com.samourai.wallet.crypto.AESUtil;
import com.samourai.wallet.util.CharSequenceX;
import com.samourai.whirlpool.cli.beans.CliProxy;
import com.samourai.whirlpool.cli.beans.CliTorExecutableMode;
import com.samourai.whirlpool.cli.utils.CliUtils;
import com.samourai.whirlpool.client.exception.NotifiableException;
import com.samourai.whirlpool.client.utils.ClientUtils;
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig;
import com.samourai.whirlpool.client.wallet.beans.ExternalDestination;
import com.samourai.whirlpool.client.wallet.beans.WhirlpoolServer;
import com.samourai.whirlpool.client.whirlpool.ServerApi;
import java.util.HashMap;
......@@ -15,6 +19,7 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import javax.validation.constraints.NotEmpty;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.util.Strings;
import org.bitcoinj.core.NetworkParameters;
import org.hibernate.validator.constraints.Range;
......@@ -44,12 +49,10 @@ public abstract class CliConfigFile {
private Optional<CliProxy> _cliProxy;
@NotEmpty private MixConfig mix;
@NotEmpty private ApiConfig api;
@NotEmpty private ExternalDestinationConfig externalDestination;
@Autowired BuildProperties buildProperties;
private static final String PUSHTX_AUTO = "auto";
private static final String PUSHTX_INTERACTIVE = "interactive";
public CliConfigFile() {
// warning: properties are NOT loaded yet
// it will be loaded later on SpringBoot application run()
......@@ -69,6 +72,7 @@ public abstract class CliConfigFile {
this.proxy = copy.proxy;
this.requestTimeout = copy.requestTimeout;
this.api = new ApiConfig(copy.api);
this.externalDestination = new ExternalDestinationConfig(copy.externalDestination);
this.mix = new MixConfig(copy.mix);
this.buildProperties = copy.buildProperties;
}
......@@ -129,6 +133,14 @@ public abstract class CliConfigFile {
this.apiKey = apiKey;
}
public ExternalDestinationConfig getExternalDestination() {
return externalDestination;
}
public void setExternalDestination(ExternalDestinationConfig externalDestination) {
this.externalDestination = externalDestination;
}
public String getSeed() {
return seed;
}
......@@ -343,6 +355,83 @@ public abstract class CliConfigFile {
}
}
public static class ExternalDestinationConfig {
@NotEmpty private String xpub;
@NotEmpty private int chain;
@NotEmpty private int startIndex;
@NotEmpty private int mixs;
@NotEmpty private int mixsRandomFactor;
public ExternalDestinationConfig() {}
public ExternalDestinationConfig(ExternalDestinationConfig copy) {
this.xpub = copy.xpub;
this.chain = copy.chain;
this.startIndex = copy.startIndex;
this.mixs = copy.mixs;
this.mixsRandomFactor = copy.mixsRandomFactor;
}
public String getXpub() {
return xpub;
}
public void setXpub(String xpub) {
this.xpub = xpub;
}
public int getChain() {
return chain;
}
public void setChain(int chain) {
this.chain = chain;
}
public int getStartIndex() {
return startIndex;
}
public void setStartIndex(int startIndex) {
this.startIndex = startIndex;
}
public int getMixs() {
return mixs;
}
public void setMixs(int mixs) {
this.mixs = mixs;
}
public int getMixsRandomFactor() {
return mixsRandomFactor;
}
public void setMixsRandomFactor(int mixsRandomFactor) {
this.mixsRandomFactor = mixsRandomFactor;
}
public Map<String, String> getConfigInfo() {
Map<String, String> configInfo = new HashMap<>();
String externalDestination =
"xpub="
+ (!StringUtils.isEmpty(xpub)
? xpub
+ ", chain="
+ chain
+ ", startIndex="
+ startIndex
+ ", mixs="
+ mixs
+ ", mixsRandomFactor="
+ mixsRandomFactor
: "null");
configInfo.put("cli/externalDestination", externalDestination);
return configInfo;
}
}
public static class TorConfig {
@NotEmpty private String executable;
private CliTorExecutableMode executableMode;
......@@ -508,7 +597,9 @@ public abstract class CliConfigFile {
protected WhirlpoolWalletConfig computeWhirlpoolWalletConfig(
IHttpClientService httpClientService,
IStompClientService stompClientService,
BackendApi backendApi) {
BackendApi backendApi,
String passphrase)
throws NotifiableException {
String serverUrl = computeServerUrl();
NetworkParameters params = server.getParams();
ServerApi serverApi = new ServerApi(serverUrl, httpClientService);
......@@ -529,10 +620,36 @@ public abstract class CliConfigFile {
config.setAutoMix(mix.isAutoMix());
config.setOverspend(mix.getOverspend());
ExternalDestination ed = computeExternalDestination(passphrase);
config.setExternalDestination(ed);
config.setResyncOnFirstRun(true);
return config;
}
private ExternalDestination computeExternalDestination(String passphrase)
throws NotifiableException {
if (!Strings.isEmpty(this.externalDestination.xpub)
&& this.externalDestination.chain >= 0
&& this.externalDestination.mixs > 0
&& this.externalDestination.mixsRandomFactor >= 0) {
try {
// decrypt externalDestination
String xpubDecrypted =
AESUtil.decrypt(this.externalDestination.xpub, new CharSequenceX(passphrase));
return new ExternalDestination(
xpubDecrypted,
this.externalDestination.chain,
this.externalDestination.startIndex,
this.externalDestination.mixs,
this.externalDestination.mixsRandomFactor);
} catch (Exception e) {
throw new NotifiableException("Invalid config value for: externalDestination.xpub");
}
}
return null;
}
public Map<String, String> getConfigInfo() {
Map<String, String> configInfo = new LinkedHashMap<>();
configInfo.put("cli/server", server.name());
......@@ -550,6 +667,7 @@ public abstract class CliConfigFile {
configInfo.put("cli/proxy", proxy != null ? ClientUtils.maskString(proxy) : "null");
configInfo.putAll(mix.getConfigInfo());
configInfo.putAll(api.getConfigInfo());
configInfo.putAll(externalDestination.getConfigInfo());
configInfo.put("cli/buildVersion", getBuildVersion() != null ? getBuildVersion() : "null");
return configInfo;
}
......
......@@ -2,7 +2,6 @@ package com.samourai.whirlpool.cli.run;
import com.samourai.wallet.client.Bip84Wallet;
import com.samourai.whirlpool.cli.ApplicationArgs;
import com.samourai.whirlpool.cli.config.CliConfig;
import com.samourai.whirlpool.cli.services.CliWalletService;
import com.samourai.whirlpool.cli.services.WalletAggregateService;
import com.samourai.whirlpool.cli.wallet.CliWallet;
......@@ -54,7 +53,16 @@ public class RunCliCommand {
}
}
public static boolean hasCommandToRun(ApplicationArgs appArgs, CliConfig cliConfig) {
return appArgs.isDumpPayload() || appArgs.isAggregatePostmix() || appArgs.isListPools();
public static String getCommandToRun(ApplicationArgs appArgs) {
if (appArgs.isDumpPayload()) {
return ApplicationArgs.ARG_DUMP_PAYLOAD;
}
if (appArgs.isAggregatePostmix()) {
return ApplicationArgs.ARG_AGGREGATE_POSTMIX;
}
if (appArgs.isListPools()) {
return appArgs.ARG_LIST_POOLS;
}
return null;
}
}
package com.samourai.whirlpool.cli.run;
import com.samourai.whirlpool.cli.ApplicationArgs;
import com.samourai.whirlpool.cli.beans.WhirlpoolPairingPayload;
import com.samourai.whirlpool.cli.services.CliConfigService;
import com.samourai.whirlpool.cli.services.CliWalletService;
import com.samourai.whirlpool.cli.utils.CliUtils;
import java.lang.invoke.MethodHandles;
import org.slf4j.Logger;
......@@ -12,17 +10,10 @@ import org.slf4j.LoggerFactory;
public class RunCliInit {
private Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private ApplicationArgs appArgs;
private CliConfigService cliConfigService;
private CliWalletService cliWalletService;
public RunCliInit(
ApplicationArgs appArgs,
CliConfigService cliConfigService,
CliWalletService cliWalletService) {
this.appArgs = appArgs;
public RunCliInit(CliConfigService cliConfigService) {
this.cliConfigService = cliConfigService;
this.cliWalletService = cliWalletService;
}
public void run() throws Exception {
......@@ -48,10 +39,7 @@ public class RunCliInit {
} else {
// samourai backend => Tor optional
log.info("⣿ • Enable Tor? (you can change this later)");
String torStr =
CliUtils.readUserInputRequired(
"Enable Tor? (y/n)", false, new String[] {"y", "n", "Y", "N"});
tor = torStr.toLowerCase().equals("y");
tor = CliUtils.readUserInputRequiredBoolean("Enable Tor? (y/n)");
log.info("⣿ ");
}
......
package com.samourai.whirlpool.cli.run;
import com.samourai.wallet.util.FormatsUtilGeneric;
import com.samourai.wallet.util.XPubUtil;
import com.samourai.whirlpool.cli.services.CliConfigService;
import com.samourai.whirlpool.cli.utils.CliUtils;
import com.samourai.whirlpool.client.exception.NotifiableException;
import java.lang.invoke.MethodHandles;
import org.apache.commons.lang3.StringUtils;
import org.bitcoinj.core.NetworkParameters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RunSetExternalXpub {
private Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final FormatsUtilGeneric formatUtil = FormatsUtilGeneric.getInstance();
private static final XPubUtil xPubUtil = XPubUtil.getInstance();
private CliConfigService cliConfigService;
public RunSetExternalXpub(CliConfigService cliConfigService) {
this.cliConfigService = cliConfigService;
}
public void run(NetworkParameters params, String passphrase) throws Exception {
log.info(CliUtils.LOG_SEPARATOR);
log.info("⣿ EXTERNAL XPUB CONFIGURATION");
log.info("⣿ This will configure an external XPub as destination for your mixed funds.");
log.info("⣿ This XPub will remain encrypted and private.");
log.info(
"⣿ It will not be shared with the Whirlpool coordinator, the Samourai backend server or your own Dojo.");
log.info("⣿ ");
// xpub
log.info("⣿ • Paste external BIP84 XPub here (or <enter> to unset current destination):");
String xpub = readXpub();
if (xpub != null) {
// chain
log.info("⣿ • Chain for XPub derivation path m/84'/<chain>' (use 0 for standard):");
int chain = CliUtils.readUserInputRequiredInt("Chain?(0)", 0, 0);
log.info("⣿ ");
// startIndex
log.info(
"⣿ • Starting index for XPub derivation path m/84'/"
+ chain
+ "'/<starting index>' (use 0 for standard):");
int startIndex = CliUtils.readUserInputRequiredInt("Starting index?(0)", 0, 0);
log.info("⣿ ");
// mixs
log.info("⣿ • Number of mixs to achieve before sending funds to XPub (>0):");
int mixs = CliUtils.readUserInputRequiredInt("Mixs?", 1);
log.info("⣿ ");
// print addresses
log.info(CliUtils.LOG_SEPARATOR);
log.info("⣿ WARNING!");
log.info(CliUtils.LOG_SEPARATOR);
log.info(
"⣿ Your funds will be automatically sent to external XPub after being mixed *at least* "
+ mixs
+ " times. This number may randomly slightly increase to improve your privacy.");
log.info("⣿ XPub: " + xpub);
log.info("⣿ Derivation path: m/84'/" + chain + "'/" + startIndex + "+'");
log.info("⣿ Sample destination addresses:");
for (int i = startIndex; i < startIndex + 3; i++) {
String address = xPubUtil.getAddressBech32(xpub, i, chain, params);
log.info("⣿ m/84'/" + chain + "'/" + i + "'" + ": " + address);
}
// validate
boolean validate = CliUtils.readUserInputRequiredBoolean("Continue? (y/n)");
if (!validate) {
throw new NotifiableException("Aborted");
}
// set configuration
cliConfigService.setExternalDestination(xpub, chain, startIndex, mixs, passphrase);
} else {
log.info("⣿ This will unset external XPub. Your funds will stay on your POSTMIX wallet.");
// validate
boolean validate = CliUtils.readUserInputRequiredBoolean("Continue? (y/n)");
if (!validate) {
throw new NotifiableException("Aborted");
}
// clear configuration
cliConfigService.clearExternalDestination();
}
log.info(CliUtils.LOG_SEPARATOR);
log.info("⣿ EXTERNAL XPUB CONFIGURATION SUCCESS");
log.info("⣿ ");
log.info("⣿ Restarting CLI...");
log.info(CliUtils.LOG_SEPARATOR);
}
private String readXpub() {
while (true) {
String input = CliUtils.readUserInput("XPub or <enter> to unset?", false);
if (StringUtils.isEmpty(input)) {
// clear current xpub
return null;
} else {
try {
if (!formatUtil.isValidXpub(input)) {
throw new NotifiableException("Invalid BIP84 XPub");
}
return input;
} catch (Exception e) {
log.error(e.getMessage());
}
log.info("⣿ ");
}
}
}
}
......@@ -2,13 +2,16 @@ package com.samourai.whirlpool.cli.services;
import com.samourai.wallet.api.pairing.PairingNetwork;
import com.samourai.wallet.api.pairing.PairingPayload;
import com.samourai.wallet.crypto.AESUtil;
import com.samourai.wallet.util.CallbackWithArg;
import com.samourai.wallet.util.CharSequenceX;
import com.samourai.whirlpool.cli.Application;
import com.samourai.whirlpool.cli.api.protocol.beans.ApiCliConfig;
import com.samourai.whirlpool.cli.beans.CliStatus;
import com.samourai.whirlpool.cli.beans.WhirlpoolPairingPayload;
import com.samourai.whirlpool.cli.config.CliConfig;
import com.samourai.whirlpool.cli.utils.CliUtils;
import com.samourai.whirlpool.cli.utils.SortedProperties;
import com.samourai.whirlpool.client.exception.NotifiableException;
import com.samourai.whirlpool.client.utils.ClientUtils;
import com.samourai.whirlpool.client.wallet.beans.WhirlpoolServer;
......@@ -41,6 +44,11 @@ public class CliConfigService {
public static final String KEY_DOJO_ENABLED = "cli.dojo.enabled";
private static final String KEY_VERSION = "cli.version";
public static final String KEY_MIX_CLIENTS = "cli.mix.clients";
private static final String KEY_EXTERNAL_DESTINATION_XPUB = "cli.externalDestination.xpub";
private static final String KEY_EXTERNAL_DESTINATION_CHAIN = "cli.externalDestination.chain";
private static final String KEY_EXTERNAL_DESTINATION_START_INDEX =
"cli.externalDestination.startIndex";
private static final String KEY_EXTERNAL_DESTINATION_MIXS = "cli.externalDestination.mixs";
private CliConfig cliConfig;
private CliStatus cliStatus;
......@@ -209,6 +217,62 @@ public class CliConfigService {
Application.restart();
}
public synchronized void setExternalDestination(
String xpub, int chain, int startIndex, int mixs, String passphrase) throws Exception {
if (log.isDebugEnabled()) {
log.debug(" • setExternalDestination");
}
if (StringUtils.isEmpty(xpub)) {
throw new NotifiableException("Invalid externalDestination.xpub");
}
// encrypt xpub
String xpubEncrypted = AESUtil.encrypt(xpub, new CharSequenceX(passphrase));
if (StringUtils.isEmpty(xpubEncrypted)) {
throw new NotifiableException("Invalid externalDestination.xpub");
}
if (chain < 0) {
throw new NotifiableException("Invalid externalDestination.chain");
}
if (startIndex < 0) {
throw new NotifiableException("Invalid externalDestination.startIndex");
}
if (mixs < 1) {
throw new NotifiableException("Invalid externalDestination.mixs");
}
// set
Properties props = loadProperties();
props.put(KEY_EXTERNAL_DESTINATION_XPUB, xpubEncrypted);
props.put(KEY_EXTERNAL_DESTINATION_CHAIN, Integer.toString(chain));
props.put(KEY_EXTERNAL_DESTINATION_START_INDEX, Integer.toString(startIndex));
props.put(KEY_EXTERNAL_DESTINATION_MIXS, Integer.toString(mixs));
// save
saveProperties(props);
// restart
Application.restart();
}
public synchronized void clearExternalDestination() throws Exception {
if (log.isDebugEnabled()) {
log.debug(" • clearExternalDestination");
}
// unset
Properties props = loadProperties();
props.remove(KEY_EXTERNAL_DESTINATION_XPUB);
props.remove(KEY_EXTERNAL_DESTINATION_CHAIN);
props.remove(KEY_EXTERNAL_DESTINATION_START_INDEX);
props.remove(KEY_EXTERNAL_DESTINATION_MIXS);
// save
saveProperties(props);
// restart
Application.restart();
}
public synchronized void setVersion(int version) throws Exception {
if (log.isDebugEnabled()) {
log.debug("setVersion: " + version);
......@@ -242,11 +306,14 @@ public class CliConfigService {
log.warn("status -> " + error);
}
protected synchronized void save(Properties props) throws Exception {
if (props.isEmpty()) {
protected synchronized void save(Properties unsortedProps) throws Exception {
if (unsortedProps.isEmpty()) {