Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rpc wallet protection endpoints #4214

Merged
merged 24 commits into from
May 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
de50692
Add rpc wallet protection endpoints
ghubstan Apr 29, 2020
2096275
Handle unlockwallet parameter errors
ghubstan Apr 30, 2020
ca68d05
Add wallet protection methods to printHelp(opts)
ghubstan Apr 30, 2020
c5fcafb
Renamed tempWalletPassword -> tempLockWalletPassword
ghubstan Apr 30, 2020
2a9d1f6
Improve gRPC error handling
ghubstan May 1, 2020
c7a6c87
Refactor grpc wallet service
cbeams May 2, 2020
6334c54
Generalize gRPC exception message cleanup
cbeams May 3, 2020
163061a
Return long vs Tuple2 from CoreWalletService#getAvailableBalance
cbeams May 3, 2020
9164579
Return void vs Tuple2 from CoreWalletService#setWalletPassword
ghubstan May 3, 2020
feafd0c
Return void vs Tuple2 from CoreWalletService#removeWalletPassword
ghubstan May 3, 2020
ab17b67
Remove obsolete try/catch block in #setWalletPassword
cbeams May 3, 2020
ec2ee45
Return void vs Tuple2 from CoreWalletService#lockWallet
ghubstan May 3, 2020
3ef7286
Return void vs Tuple2 from CoreWalletService#unlockWallet
ghubstan May 3, 2020
f5a4ca5
Add missing return statement in CoreWalletService#setWalletPassword
ghubstan May 3, 2020
b0e5da8
Refactor setwalletpassword handling to eliminate duplication
cbeams May 4, 2020
d48f9eb
Tighten up error handling
cbeams May 4, 2020
9b156b8
Remove unlockwallet timeout variable initializer
ghubstan May 4, 2020
fbb025a
Factor out two small duplcated code blocks
ghubstan May 6, 2020
4262f29
Cache temp key and crypter scrypt in unlockwallet
ghubstan May 6, 2020
24248d4
Backup wallets after each encrypt/decrypt operation
ghubstan May 6, 2020
a79ae57
Cache aesKey in unlockwallet, clear it in lockwallet
ghubstan May 7, 2020
9178ad7
Fix unlockwallet timeout override bug
ghubstan May 13, 2020
d53cc38
Wrap comments at 90 chars
cbeams May 8, 2020
7f05f37
Remove unused import
cbeams May 18, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 102 additions & 56 deletions cli/src/main/java/bisq/cli/CliMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,18 @@

package bisq.cli;

import bisq.proto.grpc.GetBalanceGrpc;
import bisq.proto.grpc.GetBalanceRequest;
import bisq.proto.grpc.GetVersionGrpc;
import bisq.proto.grpc.GetVersionRequest;
import bisq.proto.grpc.LockWalletRequest;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.UnlockWalletRequest;
import bisq.proto.grpc.WalletGrpc;

import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;

import joptsimple.OptionException;
import joptsimple.OptionParser;
import joptsimple.OptionSet;

Expand All @@ -41,6 +44,7 @@

import lombok.extern.slf4j.Slf4j;

import static java.lang.String.format;
import static java.lang.System.err;
import static java.lang.System.exit;
import static java.lang.System.out;
Expand All @@ -51,15 +55,25 @@
@Slf4j
public class CliMain {

private static final int EXIT_SUCCESS = 0;
private static final int EXIT_FAILURE = 1;

private enum Method {
getversion,
getbalance
getbalance,
lockwallet,
unlockwallet,
removewalletpassword,
setwalletpassword
}

public static void main(String[] args) {
try {
run(args);
} catch (Throwable t) {
err.println("Error: " + t.getMessage());
exit(1);
}
}

public static void run(String[] args) {
var parser = new OptionParser();

var helpOpt = parser.accepts("help", "Print this help text")
Expand All @@ -77,43 +91,33 @@ public static void main(String[] args) {
var passwordOpt = parser.accepts("password", "rpc server password")
.withRequiredArg();

OptionSet options = null;
try {
options = parser.parse(args);
} catch (OptionException ex) {
err.println("Error: " + ex.getMessage());
exit(EXIT_FAILURE);
}
OptionSet options = parser.parse(args);

if (options.has(helpOpt)) {
printHelp(parser, out);
exit(EXIT_SUCCESS);
return;
}

@SuppressWarnings("unchecked")
var nonOptionArgs = (List<String>) options.nonOptionArguments();
if (nonOptionArgs.isEmpty()) {
printHelp(parser, err);
err.println("Error: no method specified");
exit(EXIT_FAILURE);
throw new IllegalArgumentException("no method specified");
}

var methodName = nonOptionArgs.get(0);
Method method = null;
final Method method;
try {
method = Method.valueOf(methodName);
} catch (IllegalArgumentException ex) {
err.printf("Error: '%s' is not a supported method\n", methodName);
exit(EXIT_FAILURE);
throw new IllegalArgumentException(format("'%s' is not a supported method", methodName));
}

var host = options.valueOf(hostOpt);
var port = options.valueOf(portOpt);
var password = options.valueOf(passwordOpt);
if (password == null) {
err.println("Error: missing required 'password' option");
exit(EXIT_FAILURE);
}
if (password == null)
throw new IllegalArgumentException("missing required 'password' option");

var credentials = new PasswordCallCredentials(password);

Expand All @@ -122,68 +126,110 @@ public static void main(String[] args) {
try {
channel.shutdown().awaitTermination(1, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
ex.printStackTrace(err);
exit(EXIT_FAILURE);
throw new RuntimeException(ex);
}
}));

var versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials);
var walletService = WalletGrpc.newBlockingStub(channel).withCallCredentials(credentials);

try {
switch (method) {
case getversion: {
var stub = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials);
var request = GetVersionRequest.newBuilder().build();
var version = stub.getVersion(request).getVersion();
var version = versionService.getVersion(request).getVersion();
out.println(version);
exit(EXIT_SUCCESS);
return;
}
case getbalance: {
var stub = GetBalanceGrpc.newBlockingStub(channel).withCallCredentials(credentials);
var request = GetBalanceRequest.newBuilder().build();
var balance = stub.getBalance(request).getBalance();
if (balance == -1) {
err.println("Error: server is still initializing");
exit(EXIT_FAILURE);
var reply = walletService.getBalance(request);
var satoshiBalance = reply.getBalance();
var satoshiDivisor = new BigDecimal(100000000);
var btcFormat = new DecimalFormat("###,##0.00000000");
@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
var btcBalance = btcFormat.format(BigDecimal.valueOf(satoshiBalance).divide(satoshiDivisor));
out.println(btcBalance);
return;
}
case lockwallet: {
var request = LockWalletRequest.newBuilder().build();
walletService.lockWallet(request);
out.println("wallet locked");
return;
}
case unlockwallet: {
if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException("no password specified");

if (nonOptionArgs.size() < 3)
throw new IllegalArgumentException("no unlock timeout specified");

long timeout;
try {
timeout = Long.parseLong(nonOptionArgs.get(2));
} catch (NumberFormatException e) {
throw new IllegalArgumentException(format("'%s' is not a number", nonOptionArgs.get(2)));
}
out.println(formatBalance(balance));
exit(EXIT_SUCCESS);
var request = UnlockWalletRequest.newBuilder()
.setPassword(nonOptionArgs.get(1))
.setTimeout(timeout).build();
walletService.unlockWallet(request);
out.println("wallet unlocked");
return;
}
default: {
err.printf("Error: unhandled method '%s'\n", method);
exit(EXIT_FAILURE);
case removewalletpassword: {
if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException("no password specified");

var request = RemoveWalletPasswordRequest.newBuilder().setPassword(nonOptionArgs.get(1)).build();
walletService.removeWalletPassword(request);
out.println("wallet decrypted");
return;
}
case setwalletpassword: {
if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException("no password specified");

var requestBuilder = SetWalletPasswordRequest.newBuilder().setPassword(nonOptionArgs.get(1));
var hasNewPassword = nonOptionArgs.size() == 3;
if (hasNewPassword)
requestBuilder.setNewPassword(nonOptionArgs.get(2));
walletService.setWalletPassword(requestBuilder.build());
out.println("wallet encrypted" + (hasNewPassword ? " with new password" : ""));
return;
}
default: {
throw new RuntimeException(format("unhandled method '%s'", method));
}
}
} catch (StatusRuntimeException ex) {
// This exception is thrown if the client-provided password credentials do not
// match the value set on the server. The actual error message is in a nested
// exception and we clean it up a bit to make it more presentable.
Throwable t = ex.getCause() == null ? ex : ex.getCause();
err.println("Error: " + t.getMessage().replace("UNAUTHENTICATED: ", ""));
exit(EXIT_FAILURE);
// Remove the leading gRPC status code (e.g. "UNKNOWN: ") from the message
String message = ex.getMessage().replaceFirst("^[A-Z_]+: ", "");
throw new RuntimeException(message, ex);
}
}

private static void printHelp(OptionParser parser, PrintStream stream) {
try {
stream.println("Bisq RPC Client");
stream.println();
stream.println("Usage: bisq-cli [options] <method>");
stream.println("Usage: bisq-cli [options] <method> [params]");
stream.println();
parser.printHelpOn(stream);
stream.println();
stream.println("Method Description");
stream.println("------ -----------");
stream.println("getversion Get server version");
stream.println("getbalance Get server wallet balance");
stream.format("%-19s%-30s%s%n", "Method", "Params", "Description");
stream.format("%-19s%-30s%s%n", "------", "------", "------------");
stream.format("%-19s%-30s%s%n", "getversion", "", "Get server version");
stream.format("%-19s%-30s%s%n", "getbalance", "", "Get server wallet balance");
stream.format("%-19s%-30s%s%n", "lockwallet", "", "Remove wallet password from memory, locking the wallet");
stream.format("%-19s%-30s%s%n", "unlockwallet", "password timeout",
"Store wallet password in memory for timeout seconds");
stream.format("%-19s%-30s%s%n", "setwalletpassword", "password [newpassword]",
"Encrypt wallet with password, or set new password on encrypted wallet");
stream.println();
} catch (IOException ex) {
ex.printStackTrace(stream);
}
}

@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
private static String formatBalance(long satoshis) {
var btcFormat = new DecimalFormat("###,##0.00000000");
var satoshiDivisor = new BigDecimal(100000000);
return btcFormat.format(BigDecimal.valueOf(satoshis).divide(satoshiDivisor));
}
}
19 changes: 2 additions & 17 deletions core/src/main/java/bisq/core/grpc/CoreApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,13 @@

package bisq.core.grpc;

import bisq.core.btc.Balances;
import bisq.core.monetary.Price;
import bisq.core.offer.CreateOfferService;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferBookService;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OpenOfferManager;
import bisq.core.payment.PaymentAccount;
import bisq.core.presentation.BalancePresentation;
import bisq.core.trade.handlers.TransactionResultHandler;
import bisq.core.trade.statistics.TradeStatistics2;
import bisq.core.trade.statistics.TradeStatisticsManager;
Expand All @@ -49,24 +47,18 @@
*/
@Slf4j
public class CoreApi {
private final Balances balances;
private final BalancePresentation balancePresentation;
private final OfferBookService offerBookService;
private final TradeStatisticsManager tradeStatisticsManager;
private final CreateOfferService createOfferService;
private final OpenOfferManager openOfferManager;
private final User user;

@Inject
public CoreApi(Balances balances,
BalancePresentation balancePresentation,
OfferBookService offerBookService,
public CoreApi(OfferBookService offerBookService,
TradeStatisticsManager tradeStatisticsManager,
CreateOfferService createOfferService,
OpenOfferManager openOfferManager,
User user) {
this.balances = balances;
this.balancePresentation = balancePresentation;
this.offerBookService = offerBookService;
this.tradeStatisticsManager = tradeStatisticsManager;
this.createOfferService = createOfferService;
Expand All @@ -78,14 +70,6 @@ public String getVersion() {
return Version.VERSION;
}

public long getAvailableBalance() {
return balances.getAvailableBalance().get().getValue();
}

public String getAvailableBalanceAsString() {
return balancePresentation.getAvailableBalance().get();
}

public List<TradeStatistics2> getTradeStatistics() {
return new ArrayList<>(tradeStatisticsManager.getObservableTradeStatisticsSet());
}
Expand Down Expand Up @@ -160,4 +144,5 @@ public void placeOffer(String offerId,
resultHandler,
log::error);
}

}
Loading