diff --git a/build.gradle b/build.gradle index e5bffd20f4d..14065fa7080 100644 --- a/build.gradle +++ b/build.gradle @@ -62,6 +62,7 @@ configure(subprojects) { logbackVersion = '1.1.11' loggingVersion = '1.2' lombokVersion = '1.18.2' + macaroonsVersion = '0.4.1' mockitoVersion = '3.0.0' netlayerVersion = '0.6.7' protobufVersion = '3.10.0' @@ -137,7 +138,7 @@ configure([project(':cli'), unixScriptFile.text = unixScriptFile.text.replace( 'cd "`dirname \\"$PRG\\"`/.." >/dev/null', 'cd "`dirname \\"$PRG\\"`" >/dev/null') - if (applicationName == 'desktop') { + if (applicationName == 'desktop' || applicationName == 'daemon' || applicationName == 'cli') { def script = file("${rootProject.projectDir}/bisq-$applicationName") script.text = script.text.replace( 'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="-XX:MaxRAM=4g"') @@ -305,6 +306,7 @@ configure(project(':core')) { compile("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") { exclude(module: 'jackson-annotations') } + implementation "com.github.nitram509:jmacaroons:$macaroonsVersion" implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation("io.grpc:grpc-protobuf:$grpcVersion") { exclude(module: 'guava') @@ -340,6 +342,8 @@ configure(project(':cli')) { dependencies { compile project(':proto') implementation "net.sf.jopt-simple:jopt-simple:$joptVersion" + implementation "commons-codec:commons-codec:$codecVersion" + implementation "com.github.nitram509:jmacaroons:$macaroonsVersion" implementation "com.google.guava:guava:$guavaVersion" implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation("io.grpc:grpc-core:$grpcVersion") { @@ -350,7 +354,7 @@ configure(project(':cli')) { exclude(module: 'guava') exclude(module: 'animal-sniffer-annotations') } - runtimeOnly("io.grpc:grpc-netty-shaded:$grpcVersion") { + implementation("io.grpc:grpc-netty-shaded:$grpcVersion") { exclude(module: 'guava') exclude(module: 'animal-sniffer-annotations') } diff --git a/cert/aes256/generate-aes256.sh b/cert/aes256/generate-aes256.sh new file mode 100755 index 00000000000..19053f9c8b7 --- /dev/null +++ b/cert/aes256/generate-aes256.sh @@ -0,0 +1,58 @@ +#! /bin/bash + +# https://stackoverflow.com/questions/53047940/grpc-java-set-up-sslcontext-on-server +# +# https://grpc.io/docs/guides/auth +# +# To enable TLS on a server, a certificate chain and private key need to be specified in PEM format. +# Such private key should not be using a password. The order of certificates in the chain matters: +# more specifically, the certificate at the top has to be the host CA, while the one at the very +# bottom has to be the root CA. The standard TLS port is 443, but we use 8443 below to avoid +# needing extra permissions from the OS. +# +# If the issuing certificate authority is not known to the client then a properly configured +# SslContext or SSLSocketFactory should be provided to the NettyChannelBuilder or OkHttpChannelBuilder, +# respectively. + +# Set-up SSL on my GRPC server: a certificate chain and a pkcs8 private key + +# Generate CA key: + +openssl genrsa -aes256 -out ca.key 4096 + + +# Generate CA certificate: + +# Common Name (e.g. server FQDN or YOUR name) []:localhost +openssl req -new -x509 -days 365 -key ca.key -out ca.crt + +Check cert +openssl x509 -in ca.crt -text -noout + + +# Generate server key: + +openssl genrsa -aes256 -out server.key 4096 + + +# Generate server signing request: + +openssl req -new -key server.key -out server.csr +# Common Name (e.g. server FQDN or YOUR name) []:localhost + + +# Self-sign server certificate: + +openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt + + +# Remove passphrase from the server key: + +openssl rsa -in server.key -out server.key + +# Convert to pkcs8 + +openssl pkcs8 -topk8 -nocrypt -in server.key -out pkcs8_key.pem + +# -dname "cn=bisq.network, ou=None, L=YYY, ST=TTTT, o=ExampleOrg, c=AT +# Common Name (e.g. server FQDN or YOUR name) []:localhost diff --git a/cert/aes256/pkcs8_key.pem b/cert/aes256/pkcs8_key.pem new file mode 100644 index 00000000000..d5489c1602e --- /dev/null +++ b/cert/aes256/pkcs8_key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +sV/NBNgqI8wB48EZ98I0rJY57allWQ== +-----END PRIVATE KEY----- diff --git a/cert/aes256/server.crt b/cert/aes256/server.crt new file mode 100644 index 00000000000..1551b542b2f --- /dev/null +++ b/cert/aes256/server.crt @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFJjCCAw4CAQEwDQYJKoZIhvcNAQELBQAwWTELMAkGA1UEBhMCQVUxEzARBgNV +BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 +ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIwMDQwMjE0NDMyNVoXDTIxMDQwMjE0 +NDMyNVowWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNV +BAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0 +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1kiwRvRn4CZ8v+vEUSmA +65+Oo+0UaGlfv1YrlOESxZ9P8SOFjeUBhujPkRhY8zsqlYzzX3n0BkwhNHowDY0F +pGLhcLLNZjl7rSs+OOPgVw9MckP/T7y5BYdgdi89RzYkUKt6WPHqVknGbAT0Bvgi +s/TkcwtjbmtqOgMTXx4Mefjyyw/QMT5dsqjAHwU55GqhjIrLHRb3auNWG4lDbe3k +IMeJno4eRiXbuwr9s+iEOm2gnx9+W4E1qMb4fKmTkvWaYihwddN3+s6nxiS3Lu96 +dp/9SDd+JynjhWUbzlVntUSkHgZrVmYe8/35ZLrNcHIcb2nYNVb1bt6W11dE3WIP +k14Zee/bEwwswNeBmkMMlmRxyt+M2TPQSZuoQVIom33vxnK+KVeH3M7E1o0Czdgd +JfEwbooJvkW052JDDxLZaLkbECsUZWAYlADXrfzi/sbhlflyHGqAADUJdbQSz6f2 +WLrRvGftsJAH8reacSvzlUP86GzKf7RHs/eiTAxEsv6BDO06TjSBDe8w8lvbFmpd +LFCBL+BKdmzlAFY9XZLDA3lJAr/SHB2w6y9ACoZMBcgkQqfd14BvqlzPcVNOwqSH +L/H6srYJxRu35BP//aXB3VKRoJ7WkEsznFyCjsFjM6XoQlBHm1HU1qDq89gPE9h4 +/cUx5f791ya989f7RBtwM5UCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAgOif9nlf +MCM5xGiNLZoK1IKeCOXD2CNqGzpBhXKv7DnZPqhpy0+qTmGrYjX1lEO0sl7NrSoK +RWUf6iILHYoXYABXI+WjPq6btpXKuVFkG7Bd7PGEYdgXcptbwt13Jtsm+p7wg2u7 +m26tic+6E/yscWgxNE/n0ivBeKdGilBuaAtBwMKBVAwZnWArJ/Lpb3ZV2fDHpkeC +AVhF1q0bQXbPeTYNbzDrJSXinxmQOKcoR4iaVrGLrsrMF0buG1BafH9EVVysHqh/ +4pfdG1a2fknG7DV5jM14zvXSSXodlltWc6LZEekCYMQc4iJqh9Yo59zeSOzWIG0X +iPu39b+mTZOz65XXZsbbfq3wLREikXg4kV9vyKXfpY5/ydUOR7N6VLXyqoxlLH9G +V1iHW1usflXBXsN2O+ul/Gpbeo9Xt05M0hA6p/r8An3u4gR4ns/m0jmmZ6pV9TaH +lpcFKBYXPKj4C/w0Gm8msXSKPXxvifpRkIrh8Gy3ZdeDDna7FIxNJWyt7qscg+5u +k6H1i9n3tsLeI98MwwUruOfM8E+UPAQr9KUdcfeqx+SmrDOx+5vWzWDvRdWWJjg+ +1m4YxcgMKqT/sxIrgikJ5fwLOsy7t0lcxUv7rQZ8KAh7NUA6LXxcM7QqWyekIdbn +lMXFZHhZv+0N/HVxDi7AH7Ni3SBeRPBlvmw= +-----END CERTIFICATE----- diff --git a/cert/des3/generate-des3.sh b/cert/des3/generate-des3.sh new file mode 100755 index 00000000000..78fabf90e3e --- /dev/null +++ b/cert/des3/generate-des3.sh @@ -0,0 +1,57 @@ +#! /bin/bash + +# https://stackoverflow.com/questions/53047940/grpc-java-set-up-sslcontext-on-server +# +# https://grpc.io/docs/guides/auth +# +# To enable TLS on a server, a certificate chain and private key need to be specified in PEM format. +# Such private key should not be using a password. The order of certificates in the chain matters: +# more specifically, the certificate at the top has to be the host CA, while the one at the very +# bottom has to be the root CA. The standard TLS port is 443, but we use 8443 below to avoid +# needing extra permissions from the OS. +# +# If the issuing certificate authority is not known to the client then a properly configured +# SslContext or SSLSocketFactory should be provided to the NettyChannelBuilder or OkHttpChannelBuilder, +# respectively. + +# Set-up SSL on my GRPC server: a certificate chain and a pkcs8 private key + +# Generate CA key: + +openssl genrsa -des3 -out ca.key 4096 + +# Generate CA certificate: + +# Common Name (e.g. server FQDN or YOUR name) []:localhost +openssl req -new -x509 -days 365 -key ca.key -out ca.crt + +Check cert +openssl x509 -in ca.crt -text -noout + + +# Generate server key: + +openssl genrsa -des3 -out server.key 4096 + + +# Generate server signing request: + +openssl req -new -key server.key -out server.csr +# Common Name (e.g. server FQDN or YOUR name) []:localhost + + +# Self-sign server certificate: + +openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt + + +# Remove passphrase from the server key: + +openssl rsa -in server.key -out server.key + +# Convert to pkcs8 + +openssl pkcs8 -topk8 -nocrypt -in server.key -out pkcs8_key.pem + +# -dname "cn=bisq.network, ou=None, L=YYY, ST=TTTT, o=ExampleOrg, c=AT +# Common Name (e.g. server FQDN or YOUR name) []:localhost diff --git a/cli/src/main/java/bisq/cli/app/BisqCliMain.java b/cli/src/main/java/bisq/cli/app/BisqCliMain.java index 015051bdc32..d6dd787a0d6 100644 --- a/cli/src/main/java/bisq/cli/app/BisqCliMain.java +++ b/cli/src/main/java/bisq/cli/app/BisqCliMain.java @@ -18,14 +18,27 @@ package bisq.cli.app; import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; +import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; +import org.apache.commons.codec.binary.Hex; + +import javax.net.ssl.SSLException; + +import java.nio.file.Files; +import java.nio.file.Paths; + +import java.io.File; +import java.io.IOException; + import java.util.List; +import java.util.Locale; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import lombok.extern.slf4j.Slf4j; @@ -34,6 +47,7 @@ import static bisq.cli.app.CommandParser.HELP; import static bisq.cli.app.CommandParser.STOPSERVER; import static java.lang.String.format; +import static java.lang.System.err; import static java.lang.System.exit; import static java.lang.System.out; @@ -51,28 +65,31 @@ public class BisqCliMain { private final OptionParser parser; public static void main(String[] args) { - new BisqCliMain("localhost", 9998, args); - } - - private BisqCliMain(String host, int port, String[] args) { - // Channels are secure by default (via SSL/TLS); for the example disable TLS to avoid needing certificates. - this(ManagedChannelBuilder.forAddress(host, port).usePlaintext().build()); - String command = parseCommand(args); - String result = runCommand(command); - out.println(result); try { - shutdown(); // Orderly channel shutdown - } catch (InterruptedException ignored) { + String certPath = "cert/aes256/server.crt"; + NettyChannelBuilder channelBuilder = NettyChannelBuilder.forAddress("localhost", 9998) + .sslContext(GrpcSslContexts.forClient().trustManager(new File(certPath)).build()); + new BisqCliMain(channelBuilder.build(), args); + } catch (SSLException e) { + e.printStackTrace(); + exit(EXIT_FAILURE); } } /** * Construct client for accessing server using the existing channel. */ - private BisqCliMain(ManagedChannel channel) { + private BisqCliMain(ManagedChannel channel, String[] args) { this.channel = channel; - this.cmd = new CliCommand(channel); + this.cmd = new CliCommand(channel, loadMacaroon()); this.parser = new CommandParser().configure(); + String command = parseCommand(args); + String result = runCommand(command); + out.println(result); + try { + shutdown(); // Orderly channel shutdown + } catch (InterruptedException ignored) { + } } private String runCommand(String command) { @@ -109,8 +126,34 @@ private String parseCommand(String[] params) { return detectedOptions.get(0); } + private String loadMacaroon() { + String macaroonPath = appDataDirHack.get() + File.separatorChar + "bisqd.macaroon"; + try { + return Hex.encodeHexString(Files.readAllBytes(Paths.get(macaroonPath))); + } catch (IOException e) { + err.println("Error encoding authentication token " + macaroonPath); + exit(EXIT_FAILURE); + return null; + } + } + private void shutdown() throws InterruptedException { channel.shutdown().awaitTermination(1, TimeUnit.SECONDS); exit(EXIT_SUCCESS); } + + // TODO Avoid duplicating these methods from bisq.common.util.Utilities, which are not visible to :cli. + private final Supplier os = () -> System.getProperty("os.name").toLowerCase(Locale.US); + private final Supplier isLinux = () -> os.get().contains("linux"); + private final Supplier isOSX = () -> os.get().contains("mac") || os.get().contains("osx"); + private final Supplier appDataDirHack = () -> { + String userHome = System.getProperty("user.home"); + if (isLinux.get()) { + return userHome + File.separatorChar + ".local/share/Bisq"; + } else if (isOSX.get()) { + return userHome + File.separatorChar + "Library/Application Support/Bisq"; + } else { + throw new RuntimeException("OS " + os.get() + " not supported"); + } + }; } diff --git a/cli/src/main/java/bisq/cli/app/CliCommand.java b/cli/src/main/java/bisq/cli/app/CliCommand.java index e3b0bc813fe..a6e5f39b52f 100644 --- a/cli/src/main/java/bisq/cli/app/CliCommand.java +++ b/cli/src/main/java/bisq/cli/app/CliCommand.java @@ -21,6 +21,7 @@ @Slf4j final class CliCommand { + private final MacaroonCallCredential macaroonCallCredential; private final GetBalanceGrpc.GetBalanceBlockingStub getBalanceStub; private final GetVersionGrpc.GetVersionBlockingStub getVersionStub; private final StopServerGrpc.StopServerBlockingStub stopServerStub; @@ -30,10 +31,11 @@ final class CliCommand { @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") final Function prettyBalance = (sats) -> btcFormat.format(BigDecimal.valueOf(sats).divide(satoshiDivisor)); - CliCommand(ManagedChannel channel) { - getBalanceStub = GetBalanceGrpc.newBlockingStub(channel); - getVersionStub = GetVersionGrpc.newBlockingStub(channel); - stopServerStub = StopServerGrpc.newBlockingStub(channel); + CliCommand(ManagedChannel channel, String macaroon) { + this.macaroonCallCredential = new MacaroonCallCredential(macaroon); + this.getBalanceStub = GetBalanceGrpc.newBlockingStub(channel).withCallCredentials(macaroonCallCredential); + this.getVersionStub = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(macaroonCallCredential); + this.stopServerStub = StopServerGrpc.newBlockingStub(channel).withCallCredentials(macaroonCallCredential); } String getVersion() { diff --git a/cli/src/main/java/bisq/cli/app/MacaroonCallCredential.java b/cli/src/main/java/bisq/cli/app/MacaroonCallCredential.java new file mode 100644 index 00000000000..3a8b390d2ff --- /dev/null +++ b/cli/src/main/java/bisq/cli/app/MacaroonCallCredential.java @@ -0,0 +1,41 @@ +package bisq.cli.app; + +import io.grpc.CallCredentials; +import io.grpc.Metadata; + +import java.util.concurrent.Executor; + +import lombok.extern.slf4j.Slf4j; + +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; +import static io.grpc.Metadata.Key; +import static io.grpc.Status.UNAUTHENTICATED; + +@Slf4j +class MacaroonCallCredential extends CallCredentials { + + private final String macaroon; + + MacaroonCallCredential(String macaroon) { + this.macaroon = macaroon; + } + + @Override + public void applyRequestMetadata(RequestInfo requestInfo, + Executor appExecutor, + MetadataApplier metadataApplier) { + appExecutor.execute(() -> { + try { + Metadata headers = new Metadata(); + Metadata.Key macaroonKey = Key.of("macaroon", ASCII_STRING_MARSHALLER); + headers.put(macaroonKey, macaroon); + metadataApplier.apply(headers); + } catch (Throwable e) { + metadataApplier.fail(UNAUTHENTICATED.withCause(e)); + } + }); + } + + public void thisUsesUnstableApi() { + } +} diff --git a/core/src/main/java/bisq/core/app/BisqSetup.java b/core/src/main/java/bisq/core/app/BisqSetup.java index 0c1d4a638c3..e71f50f64d4 100644 --- a/core/src/main/java/bisq/core/app/BisqSetup.java +++ b/core/src/main/java/bisq/core/app/BisqSetup.java @@ -36,6 +36,7 @@ import bisq.core.dao.governance.voteresult.VoteResultService; import bisq.core.dao.state.unconfirmed.UnconfirmedBsqChangeOutputListService; import bisq.core.filter.FilterManager; +import bisq.core.grpc.MacaroonOven; import bisq.core.locale.Res; import bisq.core.notifications.MobileNotificationService; import bisq.core.notifications.alerts.DisputeMsgEvents; @@ -104,6 +105,7 @@ import org.spongycastle.crypto.params.KeyParameter; +import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -122,6 +124,10 @@ import javax.annotation.Nullable; + + +import com.github.nitram509.jmacaroons.Macaroon; + @Slf4j @Singleton public class BisqSetup { @@ -342,6 +348,7 @@ public void addBisqSetupListener(BisqSetupListener listener) { public void start() { UserThread.runPeriodically(() -> { }, 1); + maybeCreateMacaroon(); maybeReSyncSPVChain(); maybeShowTac(this::step2); } @@ -448,6 +455,13 @@ public StringProperty getP2pNetworkLabelId() { // Private /////////////////////////////////////////////////////////////////////////////////////////// + private void maybeCreateMacaroon() { + File macaroonFile = new File(config.appDataDir, "bisqd.macaroon"); + if (!macaroonFile.exists()) { + new MacaroonOven("localhost:9998", "0123456789", "bisqd rpc", macaroonFile).bake(); + } + } + private void maybeReSyncSPVChain() { // We do the delete of the spv file at startup before BitcoinJ is initialized to avoid issues with locked files under Windows. if (preferences.isResyncSpvRequested()) { diff --git a/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java b/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java new file mode 100644 index 00000000000..217d11d329a --- /dev/null +++ b/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java @@ -0,0 +1,90 @@ +package bisq.core.grpc; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; + +import org.apache.commons.codec.binary.Hex; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import java.io.File; +import java.io.IOException; + +import java.util.function.Predicate; + +import lombok.extern.slf4j.Slf4j; + +import static java.nio.charset.StandardCharsets.UTF_8; + + + +import com.github.nitram509.jmacaroons.Macaroon; +import com.github.nitram509.jmacaroons.MacaroonsBuilder; + + +@Slf4j +class AuthenticationInterceptor implements ServerInterceptor { + + private final Macaroon macaroon; + + public AuthenticationInterceptor(File appDataDir) { + this.macaroon = loadMacaroon(appDataDir); + } + + @Override + public ServerCall.Listener interceptCall( + ServerCall serverCall, + Metadata metadata, + ServerCallHandler serverCallHandler) { + authenticate(metadata); + return serverCallHandler.startCall(serverCall, metadata); + } + + private void authenticate(Metadata metadata) { + final String authToken = metadata.get(Metadata.Key.of("macaroon", Metadata.ASCII_STRING_MARSHALLER)); + if (authToken == null) { + throw new StatusRuntimeException(Status.UNAUTHENTICATED.withDescription("Authentication token is missing")); + } else { + try { + Macaroon macaroon = MacaroonsBuilder.deserialize(new String(Hex.decodeHex(authToken), UTF_8).trim()); + if (this.macaroon.signature.equals(macaroon.signature)) { + log.info("Successfully authenticated macaroon with identifier {}", macaroon.identifier); + } else { + throw new StatusRuntimeException(Status.UNAUTHENTICATED.withDescription("Authentication token is invalid")); + } + /* + TODO Compare client's macaroon.sig with server's macaroon sig (above), OR cache a secret and use verifier (below)? + MacaroonsVerifier verifier = new MacaroonsVerifier(macaroon); + if (verifier.isValid("0123456789")) { + log.info("Successfully authenticated macaroon with identifier {}", macaroon.identifier); + } else { + throw new StatusRuntimeException(Status.UNAUTHENTICATED.withDescription("Authentication token is invalid")); + } + */ + } catch (Exception e) { + throw new StatusRuntimeException(Status.UNAUTHENTICATED.withDescription(e.getMessage()).withCause(e)); + } + } + } + + private Macaroon loadMacaroon(File appDataDir) { + Predicate isValid = (p) -> p.toFile().exists() && p.toFile().length() > 0; + try { + Path macaroonPath = Paths.get(appDataDir.getAbsolutePath(), "bisqd.macaroon"); + if (isValid.test(macaroonPath)) { + String base64Macaroon = Files.readAllLines(macaroonPath, UTF_8).get(0); + return MacaroonsBuilder.deserialize(base64Macaroon); + } else { + throw new RuntimeException("gRPC server macaroon was not found in " + appDataDir.getAbsolutePath()); + } + } catch (IOException e) { + throw new RuntimeException("gRPC server macaroon could not be decoded"); + } + } +} diff --git a/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java b/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java index ef6631462f9..61d68ad9588 100644 --- a/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java +++ b/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java @@ -48,6 +48,7 @@ import io.grpc.ServerBuilder; import io.grpc.stub.StreamObserver; +import java.io.File; import java.io.IOException; import java.util.List; @@ -204,6 +205,9 @@ private void start() throws IOException { // Config services server = ServerBuilder.forPort(port) + .useTransportSecurity( // TODO find proper home for SSL cert + key + new File("cert/aes256/server.crt"), + new File("cert/aes256/pkcs8_key.pem")) .addService(new GetVersionImpl()) .addService(new GetBalanceImpl()) .addService(new GetTradeStatisticsImpl()) @@ -211,15 +215,16 @@ private void start() throws IOException { .addService(new GetPaymentAccountsImpl()) .addService(new PlaceOfferImpl()) .addService(new StopServerImpl()) + .intercept(new AuthenticationInterceptor(coreApi.getConfig().appDataDir)) .build() .start(); log.info("Server started, listening on " + port); Runtime.getRuntime().addShutdownHook(new Thread(() -> { // Use stderr here since the logger may have been reset by its JVM shutdown hook. - log.error("*** shutting down gRPC server since JVM is shutting down"); + log.error("Shutting down gRPC server"); BisqGrpcServer.this.stop(); - log.error("*** server shut down"); + log.error("Stopped"); })); } } diff --git a/core/src/main/java/bisq/core/grpc/CoreApi.java b/core/src/main/java/bisq/core/grpc/CoreApi.java index 2877849147f..737b592609b 100644 --- a/core/src/main/java/bisq/core/grpc/CoreApi.java +++ b/core/src/main/java/bisq/core/grpc/CoreApi.java @@ -32,6 +32,7 @@ import bisq.core.user.User; import bisq.common.app.Version; +import bisq.common.config.Config; import org.bitcoinj.core.Coin; @@ -49,6 +50,7 @@ */ @Slf4j public class CoreApi { + private final Config config; private final Balances balances; private final BalancePresentation balancePresentation; private final OfferBookService offerBookService; @@ -58,13 +60,15 @@ public class CoreApi { private final User user; @Inject - public CoreApi(Balances balances, + public CoreApi(Config config, + Balances balances, BalancePresentation balancePresentation, OfferBookService offerBookService, TradeStatisticsManager tradeStatisticsManager, CreateOfferService createOfferService, OpenOfferManager openOfferManager, User user) { + this.config = config; this.balances = balances; this.balancePresentation = balancePresentation; this.offerBookService = offerBookService; @@ -74,6 +78,10 @@ public CoreApi(Balances balances, this.user = user; } + public Config getConfig() { + return config; + } + public String getVersion() { return Version.VERSION; } diff --git a/core/src/main/java/bisq/core/grpc/MacaroonOven.java b/core/src/main/java/bisq/core/grpc/MacaroonOven.java new file mode 100644 index 00000000000..abf156559f6 --- /dev/null +++ b/core/src/main/java/bisq/core/grpc/MacaroonOven.java @@ -0,0 +1,57 @@ +package bisq.core.grpc; + +import bisq.common.storage.FileUtil; + +import java.io.File; +import java.io.PrintWriter; + +import lombok.extern.slf4j.Slf4j; + + + +import com.github.nitram509.jmacaroons.Macaroon; +import com.github.nitram509.jmacaroons.MacaroonsBuilder; + +@Slf4j +public class MacaroonOven { + + private final String location; // TODO what is correct bisqd hostname? + private final String secretKey; + private final String identifier; + private final File targetFile; + + public MacaroonOven(String location, String secretKey, String identifier, File targetFile) { + this.location = location; + this.secretKey = secretKey; + this.identifier = identifier; + this.targetFile = targetFile; + } + + public Macaroon bake() { + Macaroon macaroon = MacaroonsBuilder.create(location, secretKey, identifier); + persist(macaroon); + return macaroon; + } + + private void persist(Macaroon macaroon) { + File tempFile = null; + PrintWriter printWriter = null; + try { + tempFile = File.createTempFile("temp", null, targetFile.getParentFile()); + printWriter = new PrintWriter(tempFile); + printWriter.write(macaroon.serialize()); + FileUtil.renameFile(tempFile, targetFile); + } catch (Throwable t) { + log.error("could not create macaroon file " + targetFile.toString(), t); + } finally { + if (tempFile != null && tempFile.exists()) { + log.warn("temp file still exists after failed save, deleting it now"); + if (!tempFile.delete()) + log.error("cannot delete temp file"); + } + if (printWriter != null) { + printWriter.close(); + } + } + } +} diff --git a/daemon/src/main/java/resources/logback.xml b/daemon/src/main/resources/logback.xml similarity index 60% rename from daemon/src/main/java/resources/logback.xml rename to daemon/src/main/resources/logback.xml index ac5e6444ea0..5599952b141 100644 --- a/daemon/src/main/java/resources/logback.xml +++ b/daemon/src/main/resources/logback.xml @@ -11,6 +11,8 @@ - + + +