diff --git a/src/main/java/org/qortal/api/Security.java b/src/main/java/org/qortal/api/Security.java index 2449f781..448f951a 100644 --- a/src/main/java/org/qortal/api/Security.java +++ b/src/main/java/org/qortal/api/Security.java @@ -5,10 +5,20 @@ import java.net.UnknownHostException; import javax.servlet.http.HttpServletRequest; -public class Security { +import org.qortal.settings.Settings; + +public abstract class Security { + + public static final String API_KEY_HEADER = "X-API-KEY"; - // TODO: replace with proper authentication public static void checkApiCallAllowed(HttpServletRequest request) { + String expectedApiKey = Settings.getInstance().getApiKey(); + String passedApiKey = request.getHeader(API_KEY_HEADER); + + if ((expectedApiKey != null && !expectedApiKey.equals(passedApiKey)) || + (passedApiKey != null && !passedApiKey.equals(expectedApiKey))) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED); + InetAddress remoteAddr; try { remoteAddr = InetAddress.getByName(request.getRemoteAddr()); @@ -19,4 +29,5 @@ public class Security { if (!remoteAddr.isLoopbackAddress()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED); } + } diff --git a/src/main/java/org/qortal/api/resource/AddressesResource.java b/src/main/java/org/qortal/api/resource/AddressesResource.java index 39b4bd71..20e4da5a 100644 --- a/src/main/java/org/qortal/api/resource/AddressesResource.java +++ b/src/main/java/org/qortal/api/resource/AddressesResource.java @@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import java.math.BigDecimal; @@ -473,6 +474,7 @@ public class AddressesResource { } ) @ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public String computePublicize(String rawBytes58) { Security.checkApiCallAllowed(request); diff --git a/src/main/java/org/qortal/api/resource/AdminResource.java b/src/main/java/org/qortal/api/resource/AdminResource.java index 6fbadb96..fc761501 100644 --- a/src/main/java/org/qortal/api/resource/AdminResource.java +++ b/src/main/java/org/qortal/api/resource/AdminResource.java @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import java.io.IOException; @@ -133,6 +134,7 @@ public class AdminResource { ) } ) + @SecurityRequirement(name = "apiKey") public NodeStatus status() { Security.checkApiCallAllowed(request); @@ -153,6 +155,7 @@ public class AdminResource { ) } ) + @SecurityRequirement(name = "apiKey") public String shutdown() { Security.checkApiCallAllowed(request); @@ -181,6 +184,7 @@ public class AdminResource { } ) @ApiErrors({ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public ActivitySummary summary() { Security.checkApiCallAllowed(request); @@ -226,6 +230,7 @@ public class AdminResource { ) } ) + @SecurityRequirement(name = "apiKey") public Controller.StatsSnapshot getEngineStats() { Security.checkApiCallAllowed(request); @@ -244,6 +249,7 @@ public class AdminResource { } ) @ApiErrors({ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public List getMintingAccounts() { Security.checkApiCallAllowed(request); @@ -290,6 +296,7 @@ public class AdminResource { } ) @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE, ApiError.CANNOT_MINT}) + @SecurityRequirement(name = "apiKey") public String addMintingAccount(String seed58) { Security.checkApiCallAllowed(request); @@ -342,6 +349,7 @@ public class AdminResource { } ) @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public String deleteMintingAccount(String key58) { Security.checkApiCallAllowed(request); @@ -441,6 +449,7 @@ public class AdminResource { } ) @ApiErrors({ApiError.INVALID_HEIGHT, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public String orphan(String targetHeightString) { Security.checkApiCallAllowed(request); @@ -482,6 +491,7 @@ public class AdminResource { } ) @ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public String forceSync(String targetPeerAddress) { Security.checkApiCallAllowed(request); @@ -527,6 +537,7 @@ public class AdminResource { description = "Requires enough free space to rebuild repository. This will pause your node for a while." ) @ApiErrors({ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public void performRepositoryMaintenance() { Security.checkApiCallAllowed(request); diff --git a/src/main/java/org/qortal/api/resource/ApiDefinition.java b/src/main/java/org/qortal/api/resource/ApiDefinition.java index ae7de00c..f9ec7459 100644 --- a/src/main/java/org/qortal/api/resource/ApiDefinition.java +++ b/src/main/java/org/qortal/api/resource/ApiDefinition.java @@ -1,11 +1,17 @@ package org.qortal.api.resource; import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.extensions.Extension; import io.swagger.v3.oas.annotations.extensions.ExtensionProperty; import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import io.swagger.v3.oas.annotations.security.SecuritySchemes; import io.swagger.v3.oas.annotations.tags.Tag; +import org.qortal.api.Security; + @OpenAPIDefinition( info = @Info( title = "Qortal API", description = "NOTE: byte-arrays are encoded in Base58" ), tags = { @@ -30,5 +36,9 @@ import io.swagger.v3.oas.annotations.tags.Tag; }) } ) +@SecuritySchemes({ + @SecurityScheme(name = "basicAuth", type = SecuritySchemeType.HTTP, scheme = "basic"), + @SecurityScheme(name = "apiKey", type = SecuritySchemeType.APIKEY, in = SecuritySchemeIn.HEADER, paramName = Security.API_KEY_HEADER) +}) public class ApiDefinition { } \ No newline at end of file diff --git a/src/main/java/org/qortal/api/resource/ChatResource.java b/src/main/java/org/qortal/api/resource/ChatResource.java index 9a8fc8d5..6ad7d6ea 100644 --- a/src/main/java/org/qortal/api/resource/ChatResource.java +++ b/src/main/java/org/qortal/api/resource/ChatResource.java @@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; @@ -156,6 +157,7 @@ public class ChatResource { } ) @ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public String buildChat(ChatTransactionData transactionData) { Security.checkApiCallAllowed(request); @@ -203,6 +205,7 @@ public class ChatResource { } ) @ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public String buildChat(String rawBytes58) { Security.checkApiCallAllowed(request); diff --git a/src/main/java/org/qortal/api/resource/CrossChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainResource.java index c8ab6527..9e46b245 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainResource.java @@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import java.math.BigDecimal; @@ -155,6 +156,7 @@ public class CrossChainResource { } ) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_DATA, ApiError.INVALID_REFERENCE, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public String buildTrade(CrossChainBuildRequest tradeRequest) { Security.checkApiCallAllowed(request); @@ -250,6 +252,7 @@ public class CrossChainResource { } ) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public String buildTradeMessage(CrossChainTradeRequest tradeRequest) { Security.checkApiCallAllowed(request); @@ -333,6 +336,7 @@ public class CrossChainResource { } ) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_DATA, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public String buildRedeemMessage(CrossChainSecretRequest secretRequest) { Security.checkApiCallAllowed(request); @@ -404,6 +408,7 @@ public class CrossChainResource { } ) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public String buildCancelMessage(CrossChainCancelRequest cancelRequest) { Security.checkApiCallAllowed(request); @@ -459,6 +464,7 @@ public class CrossChainResource { } ) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public String deriveP2shA(CrossChainBitcoinTemplateRequest templateRequest) { Security.checkApiCallAllowed(request); @@ -485,6 +491,7 @@ public class CrossChainResource { } ) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public String deriveP2shB(CrossChainBitcoinTemplateRequest templateRequest) { Security.checkApiCallAllowed(request); @@ -542,6 +549,7 @@ public class CrossChainResource { } ) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public CrossChainBitcoinP2SHStatus checkP2shA(CrossChainBitcoinTemplateRequest templateRequest) { Security.checkApiCallAllowed(request); @@ -568,6 +576,7 @@ public class CrossChainResource { } ) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public CrossChainBitcoinP2SHStatus checkP2shB(CrossChainBitcoinTemplateRequest templateRequest) { Security.checkApiCallAllowed(request); @@ -656,6 +665,7 @@ public class CrossChainResource { ) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public String refundP2shA(CrossChainBitcoinRefundRequest refundRequest) { Security.checkApiCallAllowed(request); @@ -683,6 +693,7 @@ public class CrossChainResource { ) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public String refundP2shB(CrossChainBitcoinRefundRequest refundRequest) { Security.checkApiCallAllowed(request); @@ -793,6 +804,7 @@ public class CrossChainResource { ) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public String redeemP2shA(CrossChainBitcoinRedeemRequest redeemRequest) { Security.checkApiCallAllowed(request); @@ -821,6 +833,7 @@ public class CrossChainResource { ) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public String redeemP2shB(CrossChainBitcoinRedeemRequest redeemRequest) { Security.checkApiCallAllowed(request); @@ -935,6 +948,7 @@ public class CrossChainResource { } ) @ApiErrors({ApiError.INVALID_PRIVATE_KEY}) + @SecurityRequirement(name = "apiKey") public String getBitcoinWalletBalance(String xprv58) { Security.checkApiCallAllowed(request); @@ -969,6 +983,7 @@ public class CrossChainResource { } ) @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE}) + @SecurityRequirement(name = "apiKey") public String sendBitcoin(BitcoinSendRequest bitcoinSendRequest) { Security.checkApiCallAllowed(request); @@ -1019,6 +1034,7 @@ public class CrossChainResource { } ) @ApiErrors({ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public List getTradeBotStates() { Security.checkApiCallAllowed(request); @@ -1049,6 +1065,7 @@ public class CrossChainResource { } ) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public String tradeBotCreator(TradeBotCreateRequest tradeBotCreateRequest) { Security.checkApiCallAllowed(request); @@ -1104,6 +1121,7 @@ public class CrossChainResource { } ) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public String tradeBotResponder(TradeBotRespondRequest tradeBotRespondRequest) { Security.checkApiCallAllowed(request); @@ -1168,6 +1186,7 @@ public class CrossChainResource { } ) @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public String tradeBotDelete(String tradePrivateKey58) { Security.checkApiCallAllowed(request); diff --git a/src/main/java/org/qortal/api/resource/PeersResource.java b/src/main/java/org/qortal/api/resource/PeersResource.java index a66fef4a..70f0e3e9 100644 --- a/src/main/java/org/qortal/api/resource/PeersResource.java +++ b/src/main/java/org/qortal/api/resource/PeersResource.java @@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import java.net.InetSocketAddress; @@ -131,6 +132,7 @@ public class PeersResource { ) } ) + @SecurityRequirement(name = "apiKey") public ExecuteProduceConsume.StatsSnapshot getEngineStats() { Security.checkApiCallAllowed(request); @@ -168,6 +170,7 @@ public class PeersResource { @ApiErrors({ ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE }) + @SecurityRequirement(name = "apiKey") public String addPeer(String address) { Security.checkApiCallAllowed(request); @@ -222,6 +225,7 @@ public class PeersResource { @ApiErrors({ ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE }) + @SecurityRequirement(name = "apiKey") public String removePeer(String address) { Security.checkApiCallAllowed(request); @@ -257,6 +261,7 @@ public class PeersResource { @ApiErrors({ ApiError.REPOSITORY_ISSUE }) + @SecurityRequirement(name = "apiKey") public String removeKnownPeers(String address) { Security.checkApiCallAllowed(request); @@ -296,6 +301,7 @@ public class PeersResource { } ) @ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") public List commonBlock(String targetPeerAddress) { Security.checkApiCallAllowed(request); diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 1a989c2e..1d33dcb7 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -65,6 +65,7 @@ public class Settings { "::1", "127.0.0.1" }; private Boolean apiRestricted; + private String apiKey = null; private boolean apiLoggingEnabled = false; private boolean apiDocumentationEnabled = false; // Both of these need to be set for API to use SSL @@ -275,6 +276,9 @@ public class Settings { // Validation goes here if (this.minBlockchainPeers < 1) throwValidationError("minBlockchainPeers must be at least 1"); + + if (this.apiKey != null && this.apiKey.trim().length() < 8) + throwValidationError("apiKey must be at least 8 characters"); } // Getters / setters @@ -323,6 +327,10 @@ public class Settings { return !BlockChain.getInstance().isTestChain(); } + public String getApiKey() { + return this.apiKey; + } + public boolean isApiLoggingEnabled() { return this.apiLoggingEnabled; }