Commit a13b9af8 authored by zeroleak's avatar zeroleak
Browse files

encrypt dojoApiKey + remove PushTxService

parent 74dad036
......@@ -105,16 +105,6 @@ When tor enabled, connect to whirlpool server or wallet backend through:
- `true`: Tor hidden services
- `false`: clearnet over Tor
### PushTx
```
cli.pushtx = auto
```
Specify how to broadcast transactions (tx0, aggregate).
* auto: by default, tx are broadcasted through Samourai service.
* interactive: print raw tx and pause to let you broadcast it manually.
* http://user:password@host:port: rpc connection to your own bitcoin node (connection is not encrypted, use on trusted network only).
## API usage
whirlpool-client-cli can be managed with a REST API. See [README-API.md](README-API.md)
......
package com.samourai.rpc.client;
import com.samourai.whirlpool.client.wallet.pushTx.PushTxService;
import java.lang.invoke.MethodHandles;
import java.net.URL;
import java.util.Optional;
import org.bitcoinj.core.NetworkParameters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import wf.bitcoin.javabitcoindrpcclient.BitcoinJSONRPCClient;
import wf.bitcoin.javabitcoindrpcclient.BitcoindRpcClient;
public class JSONRpcClientServiceImpl implements RpcClientService, PushTxService {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private BitcoinJSONRPCClient rpcClient;
private NetworkParameters params;
public JSONRpcClientServiceImpl(String rpcClientUrl, NetworkParameters params) throws Exception {
log.info("Instanciating JSONRpcClientServiceImpl");
this.params = params;
try {
URL url = new URL(rpcClientUrl);
this.rpcClient = new BitcoinJSONRPCClient(url);
} catch (Exception e) {
// more understandable exception
throw new Exception("Unable to connect to RPC client");
}
}
@Override
public boolean testConnectivity() {
String nodeUrl = rpcClient.rpcURL.toString();
log.info("Connecting to bitcoin node... url=" + nodeUrl);
try {
// verify node connectivity
long blockHeight = rpcClient.getBlockCount();
// verify node network
String expectedChain = params.getPaymentProtocolId();
if (!rpcClient.getBlockChainInfo().chain().equals(expectedChain)) {
log.error(
"Invalid chain for bitcoin node: url="
+ nodeUrl
+ ", chain="
+ rpcClient.getBlockChainInfo().chain()
+ ", expectedChain="
+ expectedChain);
return false;
}
// verify blockHeight
if (blockHeight <= 0) {
log.error(
"Invalid blockHeight for bitcoin node: url="
+ nodeUrl
+ ", chain="
+ rpcClient.getBlockChainInfo().chain()
+ ", blockHeight="
+ blockHeight);
return false;
}
log.info(
"Connected to bitcoin node: url="
+ nodeUrl
+ ", chain="
+ rpcClient.getBlockChainInfo().chain()
+ ", blockHeight="
+ blockHeight);
return true;
} catch (Exception e) {
log.info("Error connecting to bitcoin node: url=" + nodeUrl + ", error=" + e.getMessage());
return false;
}
}
@Override
public Optional<RpcRawTransactionResponse> getRawTransaction(String txid) {
try {
BitcoindRpcClient.RawTransaction rawTx = rpcClient.getRawTransaction(txid);
if (rawTx == null) {
return Optional.empty();
}
RpcRawTransactionResponse rpcTxResponse =
new RpcRawTransactionResponse(rawTx.hex(), rawTx.confirmations());
return Optional.of(rpcTxResponse);
} catch (Exception e) {
log.error("getRawTransaction error", e);
return Optional.empty();
}
}
@Override
public void pushTx(String txHex) throws Exception {
if (log.isDebugEnabled()) {
log.debug("pushTx... " + txHex);
} else {
log.info("pushTx tx..." + txHex);
}
try {
rpcClient.sendRawTransaction(txHex);
} catch (Exception e) {
log.error("Unable to broadcast tx: " + txHex, e);
throw new Exception("Unable to broadcast tx: " + txHex);
}
}
}
package com.samourai.rpc.client;
import com.samourai.whirlpool.client.wallet.pushTx.PushTxService;
import java.util.Optional;
public interface RpcClientService extends PushTxService {
Optional<RpcRawTransactionResponse> getRawTransaction(String txid);
}
package com.samourai.rpc.client;
public class RpcRawTransactionResponse {
private String hex;
private int confirmations;
public RpcRawTransactionResponse(String hex, Integer confirmations) {
this.hex = hex;
this.confirmations = (confirmations != null ? confirmations : 0);
}
public String getHex() {
return hex;
}
public int getConfirmations() {
return confirmations;
}
}
......@@ -15,7 +15,6 @@ import com.samourai.whirlpool.cli.utils.CliUtils;
import com.samourai.whirlpool.cli.wallet.CliWallet;
import com.samourai.whirlpool.client.exception.NotifiableException;
import com.samourai.whirlpool.client.utils.LogbackUtils;
import com.samourai.whirlpool.client.wallet.pushTx.PushTxService;
import java.lang.invoke.MethodHandles;
import java.util.Arrays;
import java.util.Map;
......@@ -53,7 +52,6 @@ public class Application implements ApplicationRunner {
@Autowired private CliConfigService cliConfigService;
@Autowired private CliWalletService cliWalletService;
private static CliWalletService cliWalletServiceStatic;
@Autowired private PushTxService pushTxService;
@Autowired private Bech32UtilGeneric bech32Util;
@Autowired private WalletAggregateService walletAggregateService;
@Autowired private CliTorClientService cliTorClientService;
......@@ -168,11 +166,6 @@ public class Application implements ApplicationRunner {
return;
}
// check pushTxService
if (!pushTxService.testConnectivity()) {
throw new NotifiableException("Unable to connect to pushTxService");
}
// check cli initialized
if (cliConfigService.isCliStatusNotInitialized()) {
// not initialized
......@@ -227,7 +220,7 @@ public class Application implements ApplicationRunner {
if (RunCliCommand.hasCommandToRun(appArgs, cliConfig)) {
// execute specific command
new RunCliCommand(appArgs, cliWalletService, walletAggregateService, cliConfig).run();
new RunCliCommand(appArgs, cliWalletService, walletAggregateService).run();
} else {
// start wallet
cliWallet.start();
......
......@@ -29,7 +29,6 @@ public class ApplicationArgs {
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_PUSHTX = "pushtx";
private static final String ARG_LISTEN = "listen";
private static final String ARG_API_KEY = "api-key";
private static final String ARG_INIT = "init";
......@@ -59,11 +58,6 @@ public class ApplicationArgs {
cliConfig.getMix().setAutoMix(valueBool);
}
value = optionalOption(ARG_PUSHTX);
if (value != null) {
cliConfig.setPushtx(value);
}
valueInt = optionalInt(ARG_CLIENTS);
if (valueInt != null) {
cliConfig.getMix().setClients(valueInt);
......
......@@ -25,7 +25,6 @@ public abstract class CliConfigFile {
private int version; // 0 for versions < 1
private WhirlpoolServer server;
private String scode;
@NotEmpty private String pushtx;
@NotEmpty private boolean tor;
@NotEmpty private TorConfig torConfig;
@NotEmpty private DojoConfig dojo;
......@@ -50,7 +49,6 @@ public abstract class CliConfigFile {
this.version = copy.version;
this.server = copy.server;
this.scode = copy.scode;
this.pushtx = copy.pushtx;
this.tor = copy.tor;
this.torConfig = new TorConfig(copy.torConfig);
this.dojo = new DojoConfig(copy.dojo);
......@@ -87,26 +85,6 @@ public abstract class CliConfigFile {
this.scode = scode;
}
public boolean isPushtxInteractive() {
return PUSHTX_INTERACTIVE.equals(pushtx);
}
public boolean isPushtxAuto() {
return PUSHTX_AUTO.equals(pushtx);
}
public boolean isPushtxCli() {
return !PUSHTX_INTERACTIVE.equals(pushtx) && !PUSHTX_AUTO.equals(pushtx);
}
public String getPushtx() {
return pushtx;
}
public void setPushtx(String pushtx) {
this.pushtx = pushtx;
}
public boolean getTor() {
return tor;
}
......@@ -411,7 +389,6 @@ public abstract class CliConfigFile {
Map<String, String> configInfo = new LinkedHashMap<>();
configInfo.put("cli/server", server.name());
configInfo.put("cli/scode", scode);
configInfo.put("cli/pushtx", ClientUtils.maskString(pushtx));
configInfo.put("cli/tor", Boolean.toString(tor));
configInfo.putAll(torConfig.getConfigInfo());
if (dojo != null) {
......
......@@ -2,10 +2,7 @@ package com.samourai.whirlpool.cli.config;
import com.samourai.wallet.hd.java.HD_WalletFactoryJava;
import com.samourai.wallet.segwit.bech32.Bech32UtilGeneric;
import com.samourai.whirlpool.cli.services.CliPushTxService;
import com.samourai.whirlpool.cli.services.SamouraiApiService;
import com.samourai.whirlpool.client.tx0.Tx0Service;
import com.samourai.whirlpool.client.wallet.pushTx.PushTxService;
import java.lang.invoke.MethodHandles;
import org.bitcoinj.core.NetworkParameters;
import org.slf4j.Logger;
......@@ -46,11 +43,6 @@ public class CliServicesConfig {
return cliConfig.getServer().getParams();
}
@Bean
PushTxService pushTxService(CliConfig cliConfig, SamouraiApiService samouraiApiService) {
return new CliPushTxService(cliConfig, samouraiApiService);
}
@Bean
Tx0Service tx0Service(CliConfig cliConfig) {
return new Tx0Service(cliConfig.getServer().getParams());
......
......@@ -16,17 +16,14 @@ public class RunCliCommand {
private ApplicationArgs appArgs;
private CliWalletService cliWalletService;
private WalletAggregateService walletAggregateService;
private CliConfig cliConfig;
public RunCliCommand(
ApplicationArgs appArgs,
CliWalletService cliWalletService,
WalletAggregateService walletAggregateService,
CliConfig cliConfig) {
WalletAggregateService walletAggregateService) {
this.appArgs = appArgs;
this.cliWalletService = cliWalletService;
this.walletAggregateService = walletAggregateService;
this.cliConfig = cliConfig;
}
public void run() throws Exception {
......@@ -43,10 +40,11 @@ public class RunCliCommand {
if (toAddress != null && !"true".equals(toAddress)) {
Bip84ApiWallet depositWallet = cliWallet.getWalletDeposit();
log.info(" • Moving funds to: " + toAddress);
walletAggregateService.toAddress(depositWallet, toAddress);
walletAggregateService.toAddress(depositWallet, toAddress, cliWallet);
}
} else if (appArgs.isListPools()) {
new RunListPools(cliWalletService, cliConfig).run();
CliWallet cliWallet = cliWalletService.getSessionWallet();
new RunListPools(cliWallet).run();
} else {
throw new Exception("Unknown command.");
}
......
package com.samourai.whirlpool.cli.run;
import com.samourai.whirlpool.cli.config.CliConfig;
import com.samourai.whirlpool.cli.services.CliWalletService;
import com.samourai.whirlpool.cli.wallet.CliWallet;
import com.samourai.whirlpool.client.utils.ClientUtils;
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
import com.samourai.whirlpool.client.whirlpool.beans.Pools;
import java.lang.invoke.MethodHandles;
import java.util.Collection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RunListPools {
private Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private CliWalletService cliWalletService;
private CliConfig cliConfig;
private CliWallet cliWallet;
public RunListPools(CliWalletService cliWalletService, CliConfig cliConfig) {
this.cliWalletService = cliWalletService;
this.cliConfig = cliConfig;
public RunListPools(CliWallet cliWallet) {
this.cliWallet = cliWallet;
}
public void run() throws Exception {
Pools pools = cliWalletService.listPools(cliConfig);
Collection<Pool> pools = cliWallet.getPools(true);
// show available pools
String lineFormat = "| %15s | %6s | %15s | %14s | %12s | %15s | %23s |\n";
......@@ -39,7 +36,7 @@ public class RunListPools {
sb.append(
String.format(
lineFormat, "", "(btc)", "", "(confir/reg)", "", "(target/min)", "min-max (sat)"));
for (Pool pool : pools.getPools()) {
for (Pool pool : pools) {
sb.append(
String.format(
lineFormat,
......
......@@ -88,11 +88,11 @@ public class CliConfigService {
// use dojo?
String dojoUrl = null;
String dojoApiKey = null;
String dojoApiKeyEncrypted = null;
PairingPayload.PairingDojo pairingDojo = pairingWallet.getDojo();
if (pairingDojo != null) {
dojoUrl = pairingDojo.getUrl();
dojoApiKey = pairingDojo.getApikey();
dojoApiKeyEncrypted = pairingDojo.getApikey();
if (dojo == null) {
dojo = true;
}
......@@ -116,7 +116,13 @@ public class CliConfigService {
: WhirlpoolServer.TESTNET;
return initialize(
encryptedMnemonic, appendPassphrase, whirlpoolServer, tor, dojoUrl, dojoApiKey, dojo);
encryptedMnemonic,
appendPassphrase,
whirlpoolServer,
tor,
dojoUrl,
dojoApiKeyEncrypted,
dojo);
}
public synchronized String initialize(
......@@ -125,7 +131,7 @@ public class CliConfigService {
WhirlpoolServer whirlpoolServer,
boolean tor,
String dojoUrl,
String dojoApiKey,
String dojoApiKeyEncrypted,
boolean dojoEnabled)
throws NotifiableException {
if (log.isDebugEnabled()) {
......@@ -152,8 +158,8 @@ public class CliConfigService {
if (dojoUrl != null) {
props.put(KEY_DOJO_URL, dojoUrl);
}
if (dojoApiKey != null) {
props.put(KEY_DOJO_APIKEY, dojoApiKey);
if (dojoApiKeyEncrypted != null) {
props.put(KEY_DOJO_APIKEY, dojoApiKeyEncrypted);
}
props.put(KEY_DOJO_ENABLED, Boolean.toString(dojoEnabled));
try {
......
package com.samourai.whirlpool.cli.services;
import com.samourai.rpc.client.JSONRpcClientServiceImpl;
import com.samourai.rpc.client.RpcClientService;
import com.samourai.whirlpool.cli.config.CliConfig;
import com.samourai.whirlpool.client.wallet.pushTx.PushTxService;
import java.lang.invoke.MethodHandles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
// PushTxService wrapper for watching for cliConfig changes
@Service
public class CliPushTxService implements PushTxService {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private CliConfig cliConfig;
private SamouraiApiService samouraiApiService;
private PushTxService pushTxService;
public CliPushTxService(CliConfig cliConfig, SamouraiApiService samouraiApiService) {
this.cliConfig = cliConfig;
this.samouraiApiService = samouraiApiService;
this.pushTxService = new InteractivePushTxService();
}
private PushTxService get() throws Exception {
if (cliConfig.isPushtxInteractive() && !(pushTxService instanceof InteractivePushTxService)) {
if (log.isDebugEnabled()) {
log.debug("pushtx config changed: interactive");
}
pushTxService = new InteractivePushTxService();
}
if (cliConfig.isPushtxCli() && !(pushTxService instanceof RpcClientService)) {
if (log.isDebugEnabled()) {
log.debug("pushtx config changed: rpc");
}
String rpcClientUrl = cliConfig.getPushtx();
pushTxService = new JSONRpcClientServiceImpl(rpcClientUrl, cliConfig.getServer().getParams());
}
if (cliConfig.isPushtxAuto() && !(pushTxService instanceof SamouraiApiService)) {
if (log.isDebugEnabled()) {
log.debug("pushtx config changed: auto");
}
pushTxService = samouraiApiService;
}
return pushTxService;
}
@Override
public void pushTx(String txHex) throws Exception {
get().pushTx(txHex);
}
@Override
public boolean testConnectivity() {
try {
return get().testConnectivity();
} catch (Exception e) {
log.error("", e);
return false;
}
}
}
......@@ -24,7 +24,6 @@ import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig;
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletService;
import com.samourai.whirlpool.client.wallet.persist.FileWhirlpoolWalletPersistHandler;
import com.samourai.whirlpool.client.wallet.persist.WhirlpoolWalletPersistHandler;
import com.samourai.whirlpool.client.whirlpool.beans.Pools;
import java.io.File;
import java.lang.invoke.MethodHandles;
import java.util.Map;
......@@ -50,7 +49,6 @@ public class CliWalletService extends WhirlpoolWalletService {
private JavaHttpClient httpClient;
private JavaStompClientService stompClientService;
private CliTorClientService cliTorClientService;
private SamouraiApiService samouraiApiService;
// available when wallet is opened
private CliWallet sessionWallet = null;
......@@ -62,8 +60,7 @@ public class CliWalletService extends WhirlpoolWalletService {
WalletAggregateService walletAggregateService,
JavaHttpClient httpClient,
JavaStompClientService stompClientService,
CliTorClientService cliTorClientService,
SamouraiApiService samouraiApiService) {
CliTorClientService cliTorClientService) {
super();
this.cliConfig = cliConfig;
this.cliConfigService = cliConfigService;
......@@ -72,10 +69,9 @@ public class CliWalletService extends WhirlpoolWalletService {
this.httpClient = httpClient;
this.stompClientService = stompClientService;
this.cliTorClientService = cliTorClientService;
this.samouraiApiService = samouraiApiService;
}
public CliWallet openWallet(String seedPassphrase) throws Exception {
public CliWallet openWallet(String passphrase) throws Exception {
// require CliStatus.READY
if (!CliStatus.READY.equals(cliConfigService.getCliStatus())) {
throw new NotifiableException(
......@@ -84,9 +80,10 @@ public class CliWalletService extends WhirlpoolWalletService {
NetworkParameters params = cliConfig.getServer().getParams();
// decrypt seed
String seedWords;
try {
seedWords = decryptSeedWords(cliConfig.getSeed(), seedPassphrase);
seedWords = decryptSeedWords(cliConfig.getSeed(), passphrase);
} catch (Exception e) {
log.error("decryptSeedWords failed, invalid passphrase?");
if (log.isDebugEnabled()
......@@ -102,11 +99,11 @@ public class CliWalletService extends WhirlpoolWalletService {
try {
// init wallet from seed
byte[] seed = hdWalletFactory.computeSeedFromWords(seedWords);
String walletPassphrase = cliConfig.isSeedAppendPassphrase() ? seedPassphrase : "";
String walletPassphrase = cliConfig.isSeedAppendPassphrase() ? passphrase : "";
bip84w = hdWalletFactory.getBIP84(seed, walletPassphrase, params);
// identifier
walletIdentifier = computeWalletIdentifier(seed, seedPassphrase, params);
walletIdentifier = computeWalletIdentifier(seed, passphrase, params);
} catch (MnemonicException e) {
throw new NotifiableException("Mnemonic failed, invalid passphrase?");
}
......@@ -119,6 +116,13 @@ public class CliWalletService extends WhirlpoolWalletService {
}
}
// backend connexion
SamouraiApiService samouraiApiService = computeSamouraiApiService(passphrase);
if (!samouraiApiService.testConnectivity()) {
throw new NotifiableException(
"Unable to connect to wallet backend: " + samouraiApiService.getUrlBackend());
}
// open wallet
WhirlpoolWalletPersistHandler persistHandler = computePersistHandler(walletIdentifier);
WhirlpoolWalletConfig whirlpoolWalletConfig =
......@@ -137,6 +141,15 @@ public class CliWalletService extends WhirlpoolWalletService {
return sessionWallet;
}
private SamouraiApiService computeSamouraiApiService(String passphrase) throws Exception {
// decrypt apiKey if any
String dojoApiKey = cliConfig.computeBackendApiKey();
if (dojoApiKey != null) {
dojoApiKey = decryptApiKey(dojoApiKey, passphrase);
}
return new SamouraiApiService(httpClient, cliConfig, dojoApiKey);
}
private WhirlpoolWalletPersistHandler computePersistHandler(String walletIdentifier)
throws NotifiableException {
File indexFile = computeIndexFile(walletIdentifier);
......@@ -151,6 +164,10 @@ public class CliWalletService extends WhirlpoolWalletService {
return AESUtil.decrypt(seedWordsEncrypted, new CharSequenceX(seedPassphrase));
}
protected String decryptApiKey(String apiKey, String seedPassphrase) throws Exception {
return AESUtil.decrypt(apiKey, new CharSequenceX(seedPassphrase));
}
public void closeWallet() {
if (this.sessionWallet != null) {
this.sessionWallet.stop();
......@@ -237,11 +254,4 @@ public class CliWalletService extends WhirlpoolWalletService {
String json = ClientUtils.toJsonString(pairingPayload);
return json;
}
public Pools listPools(CliConfig cliConfig) throws Exception {
WhirlpoolWalletConfig config =
cliConfig.computeWhirlpoolWalletConfig(
httpClient, stompClientService, null, samouraiApiService);
return config.newClient().fetchPools();
}