qortal/src/main/java/api/resource/UtilsResource.java
catbref c4ed4b378c Refactoring, new translations, cleaning up warnings.
Refactored to standard Maven layout:
src/main/java
src/main/resources
src/test/java
etc.

New translation code that uses locale-specific ResourceBundles
to load translations on demand.

Reworked API error/exceptions code to a shorter, simpler
@ApiErrors annotation. Processing of @ApiErrors annotations
produces an example for each possible API error and includes
API error string in HTTP response code, e.g.
400 INVALID_SIGNATURE
Missing API error cases added to each API call.

Translation of openAPI.json removed (for now).

block-explorer.html and BIP39 wordlists now read as resources
instead of direct from disk.

Java compile warnings fixed.
Some runtime warnings remain:

WARNING: A provider api.resource.ApiDefinition registered in SERVER runtime does not implement any provider interfaces applicable in the SERVER runtime.
WARNING: A provider api.resource.AnnotationPostProcessor registered in SERVER runtime does not implement any provider interfaces applicable in the SERVER runtime.
WARN org.reflections.Reflections - given scan urls are empty. set urls in the configuration
2018-12-21 11:14:16 +00:00

375 lines
8.8 KiB
Java

package api.resource;
import io.swagger.v3.oas.annotations.Operation;
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.tags.Tag;
import qora.account.PrivateKeyAccount;
import qora.crypto.Crypto;
import utils.BIP39;
import utils.Base58;
import utils.NTP;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Longs;
import api.ApiError;
import api.ApiErrors;
import api.ApiExceptionFactory;
@Path("/utils")
@Produces({
MediaType.TEXT_PLAIN
})
@Tag(
name = "Utilities"
)
public class UtilsResource {
@Context
HttpServletRequest request;
@POST
@Path("/fromBase64")
@Operation(
summary = "Convert base64 data to hex",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
),
responses = {
@ApiResponse(
description = "hex string",
content = @Content(
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
public String fromBase64(String base64) {
try {
return HashCode.fromBytes(Base64.getDecoder().decode(base64.trim())).toString();
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
}
@POST
@Path("/fromBase58")
@Operation(
summary = "Convert base58 data to hex",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
),
responses = {
@ApiResponse(
description = "hex string",
content = @Content(
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
public String base64from58(String base58) {
try {
return HashCode.fromBytes(Base58.decode(base58.trim())).toString();
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
}
@GET
@Path("/toBase64/{hex}")
@Operation(
summary = "Convert hex to base64",
responses = {
@ApiResponse(
description = "base64",
content = @Content(
schema = @Schema(
type = "string"
)
)
)
}
)
public String toBase64(@PathParam("hex") String hex) {
return Base64.getEncoder().encodeToString(HashCode.fromString(hex).asBytes());
}
@GET
@Path("/toBase58/{hex}")
@Operation(
summary = "Convert hex to base58",
responses = {
@ApiResponse(
description = "base58",
content = @Content(
schema = @Schema(
type = "string"
)
)
)
}
)
public String toBase58(@PathParam("hex") String hex) {
return Base58.encode(HashCode.fromString(hex).asBytes());
}
@GET
@Path("/random")
@Operation(
summary = "Generate random data",
description = "Optionally pass data length, defaults to 32 bytes.",
responses = {
@ApiResponse(
description = "base58 data",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
public String random(@QueryParam("length") Integer length) {
if (length == null)
length = 32;
byte[] random = new byte[length];
new SecureRandom().nextBytes(random);
return Base58.encode(random);
}
@GET
@Path("/mnemonic")
@Operation(
summary = "Generate 12-word BIP39 mnemonic",
description = "Optionally pass 16-byte, base58-encoded entropy or entropy will be internally generated.<br>"
+ "Example entropy input: YcVfxkQb6JRzqk5kF2tNLv",
responses = {
@ApiResponse(
description = "mnemonic",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
public String getMnemonic(@QueryParam("entropy") String suppliedEntropy) {
/*
* BIP39 word lists have 2048 entries so can be represented by 11 bits.
* UUID (128bits) and another 4 bits gives 132 bits.
* 132 bits, divided by 11, gives 12 words.
*/
byte[] entropy;
if (suppliedEntropy != null) {
// Use caller-supplied entropy input
try {
entropy = Base58.decode(suppliedEntropy);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
// Must be 16-bytes
if (entropy.length != 16)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
} else {
// Generate entropy internally
UUID uuid = UUID.randomUUID();
byte[] uuidMSB = Longs.toByteArray(uuid.getMostSignificantBits());
byte[] uuidLSB = Longs.toByteArray(uuid.getLeastSignificantBits());
entropy = Bytes.concat(uuidMSB, uuidLSB);
}
// Use SHA256 to generate more bits
byte[] hash = Crypto.digest(entropy);
// Append first 4 bits from hash to end. (Actually 8 bits but we only use 4).
byte checksum = (byte) (hash[0] & 0xf0);
entropy = Bytes.concat(entropy, new byte[] {
checksum
});
return BIP39.encode(entropy, "en");
}
@POST
@Path("/mnemonic")
@Operation(
summary = "Calculate binary entropy from 12-word BIP39 mnemonic",
description = "Returns the base58-encoded binary form, or \"false\" if mnemonic is invalid.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
),
responses = {
@ApiResponse(
description = "entropy in base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
public String fromMnemonic(String mnemonic) {
if (mnemonic.isEmpty())
return "false";
// Strip leading/trailing whitespace if any
mnemonic = mnemonic.trim();
String[] phraseWords = mnemonic.split(" ");
if (phraseWords.length != 12)
return "false";
// Convert BIP39 mnemonic to binary
byte[] binary = BIP39.decode(phraseWords, "en");
if (binary == null)
return "false";
byte[] entropy = Arrays.copyOf(binary, 16); // 132 bits is 16.5 bytes, but we're discarding checksum nybble
byte checksumNybble = (byte) (binary[16] & 0xf0);
byte[] checksum = Crypto.digest(entropy);
if (checksumNybble != (byte) (checksum[0] & 0xf0))
return "false";
return Base58.encode(entropy);
}
@GET
@Path("/privateKey/{entropy}")
@Operation(
summary = "Calculate private key from supplied 16-byte entropy",
responses = {
@ApiResponse(
description = "private key in base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
public String privateKey(@PathParam("entropy") String entropy58) {
byte[] entropy;
try {
entropy = Base58.decode(entropy58);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
if (entropy.length != 16)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
byte[] privateKey = Crypto.digest(entropy);
return Base58.encode(privateKey);
}
@GET
@Path("/publicKey/{privateKey}")
@Operation(
summary = "Calculate public key from supplied 32-byte private key",
responses = {
@ApiResponse(
description = "public key in base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
public String publicKey(@PathParam("privateKey") String privateKey58) {
byte[] privateKey;
try {
privateKey = Base58.decode(privateKey58);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
if (privateKey.length != 32)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
byte[] publicKey = new PrivateKeyAccount(null, privateKey).getPublicKey();
return Base58.encode(publicKey);
}
@GET
@Path("/timestamp")
@Operation(
summary = "Returns current timestamp as milliseconds from unix epoch",
responses = {
@ApiResponse(
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number"
)
)
)
}
)
public long getTimestamp() {
return NTP.getTime();
}
}