mirror of
https://github.com/Qortal/qortal.git
synced 2025-04-23 11:27:51 +00:00
API: terminology corrections and more utility calls
This commit is contained in:
parent
e1dbaa5597
commit
68f99cfc11
@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import qora.account.PrivateKeyAccount;
|
||||||
import qora.crypto.Crypto;
|
import qora.crypto.Crypto;
|
||||||
import utils.BIP39;
|
import utils.BIP39;
|
||||||
import utils.Base58;
|
import utils.Base58;
|
||||||
@ -13,23 +14,22 @@ import utils.Base58;
|
|||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.POST;
|
import javax.ws.rs.POST;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.QueryParam;
|
import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashCode;
|
||||||
import com.google.common.primitives.Bytes;
|
import com.google.common.primitives.Bytes;
|
||||||
import com.google.common.primitives.Longs;
|
import com.google.common.primitives.Longs;
|
||||||
|
|
||||||
import globalization.BIP39WordList;
|
|
||||||
|
|
||||||
@Path("/utils")
|
@Path("/utils")
|
||||||
@Produces({
|
@Produces({
|
||||||
MediaType.TEXT_PLAIN
|
MediaType.TEXT_PLAIN
|
||||||
@ -43,9 +43,9 @@ public class UtilsResource {
|
|||||||
HttpServletRequest request;
|
HttpServletRequest request;
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/base58from64")
|
@Path("/fromBase64")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert base64 data to base58",
|
summary = "Convert base64 data to hex",
|
||||||
requestBody = @RequestBody(
|
requestBody = @RequestBody(
|
||||||
required = true,
|
required = true,
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@ -57,7 +57,7 @@ public class UtilsResource {
|
|||||||
),
|
),
|
||||||
responses = {
|
responses = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
description = "base58 data",
|
description = "hex string",
|
||||||
content = @Content(
|
content = @Content(
|
||||||
schema = @Schema(
|
schema = @Schema(
|
||||||
type = "string"
|
type = "string"
|
||||||
@ -66,18 +66,18 @@ public class UtilsResource {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
public String base58from64(String base64) {
|
public String fromBase64(String base64) {
|
||||||
try {
|
try {
|
||||||
return Base58.encode(Base64.getDecoder().decode(base64.trim()));
|
return HashCode.fromBytes(Base64.getDecoder().decode(base64.trim())).toString();
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
|
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/base64from58")
|
@Path("/fromBase58")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert base58 data to base64",
|
summary = "Convert base58 data to hex",
|
||||||
requestBody = @RequestBody(
|
requestBody = @RequestBody(
|
||||||
required = true,
|
required = true,
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@ -89,7 +89,7 @@ public class UtilsResource {
|
|||||||
),
|
),
|
||||||
responses = {
|
responses = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
description = "base64 data",
|
description = "hex string",
|
||||||
content = @Content(
|
content = @Content(
|
||||||
schema = @Schema(
|
schema = @Schema(
|
||||||
type = "string"
|
type = "string"
|
||||||
@ -100,17 +100,55 @@ public class UtilsResource {
|
|||||||
)
|
)
|
||||||
public String base64from58(String base58) {
|
public String base64from58(String base58) {
|
||||||
try {
|
try {
|
||||||
return Base64.getEncoder().encodeToString(Base58.decode(base58.trim()));
|
return HashCode.fromBytes(Base58.decode(base58.trim())).toString();
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
|
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/seed")
|
@Path("/toBase64/{hex}")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Generate random seed",
|
summary = "Convert hex to base64",
|
||||||
description = "Optionally pass seed length, defaults to 32 bytes.",
|
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 = {
|
responses = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
description = "base58 data",
|
description = "base58 data",
|
||||||
@ -123,24 +161,24 @@ public class UtilsResource {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
public String seed(@QueryParam("length") Integer length) {
|
public String random(@QueryParam("length") Integer length) {
|
||||||
if (length == null)
|
if (length == null)
|
||||||
length = 32;
|
length = 32;
|
||||||
|
|
||||||
byte[] seed = new byte[length];
|
byte[] random = new byte[length];
|
||||||
new SecureRandom().nextBytes(seed);
|
new SecureRandom().nextBytes(random);
|
||||||
return Base58.encode(seed);
|
return Base58.encode(random);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/seedPhrase")
|
@Path("/mnemonic")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Generate random 12-word BIP39 seed phrase",
|
summary = "Generate 12-word BIP39 mnemonic",
|
||||||
description = "Optionally pass 16-byte, base58-encoded entropy input or entropy will be internally generated.<br>"
|
description = "Optionally pass 16-byte, base58-encoded entropy or entropy will be internally generated.<br>"
|
||||||
+ "Example entropy input: YcVfxkQb6JRzqk5kF2tNLv",
|
+ "Example entropy input: YcVfxkQb6JRzqk5kF2tNLv",
|
||||||
responses = {
|
responses = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
description = "seed phrase",
|
description = "mnemonic",
|
||||||
content = @Content(
|
content = @Content(
|
||||||
mediaType = MediaType.TEXT_PLAIN,
|
mediaType = MediaType.TEXT_PLAIN,
|
||||||
schema = @Schema(
|
schema = @Schema(
|
||||||
@ -150,25 +188,23 @@ public class UtilsResource {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
public String seedPhrase(@QueryParam("entropy") String input) {
|
public String getMnemonic(@QueryParam("entropy") String suppliedEntropy) {
|
||||||
/*
|
/*
|
||||||
* BIP39 word lists have 2048 entries so can be represented by 11 bits.
|
* BIP39 word lists have 2048 entries so can be represented by 11 bits.
|
||||||
* UUID (128bits) and another 4 bits gives 132 bits.
|
* UUID (128bits) and another 4 bits gives 132 bits.
|
||||||
* 132 bits, divided by 11, gives 12 words.
|
* 132 bits, divided by 11, gives 12 words.
|
||||||
*/
|
*/
|
||||||
final int BITS_PER_WORD = 11;
|
byte[] entropy;
|
||||||
|
if (suppliedEntropy != null) {
|
||||||
byte[] message;
|
|
||||||
if (input != null) {
|
|
||||||
// Use caller-supplied entropy input
|
// Use caller-supplied entropy input
|
||||||
try {
|
try {
|
||||||
message = Base58.decode(input);
|
entropy = Base58.decode(suppliedEntropy);
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
|
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must be 16-bytes
|
// Must be 16-bytes
|
||||||
if (message.length != 16)
|
if (entropy.length != 16)
|
||||||
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
|
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
|
||||||
} else {
|
} else {
|
||||||
// Generate entropy internally
|
// Generate entropy internally
|
||||||
@ -176,26 +212,26 @@ public class UtilsResource {
|
|||||||
|
|
||||||
byte[] uuidMSB = Longs.toByteArray(uuid.getMostSignificantBits());
|
byte[] uuidMSB = Longs.toByteArray(uuid.getMostSignificantBits());
|
||||||
byte[] uuidLSB = Longs.toByteArray(uuid.getLeastSignificantBits());
|
byte[] uuidLSB = Longs.toByteArray(uuid.getLeastSignificantBits());
|
||||||
message = Bytes.concat(uuidMSB, uuidLSB);
|
entropy = Bytes.concat(uuidMSB, uuidLSB);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use SHA256 to generate more bits
|
// Use SHA256 to generate more bits
|
||||||
byte[] hash = Crypto.digest(message);
|
byte[] hash = Crypto.digest(entropy);
|
||||||
|
|
||||||
// Append first 4 bits from hash to end. (Actually 8 bits but we only use 4).
|
// Append first 4 bits from hash to end. (Actually 8 bits but we only use 4).
|
||||||
byte checksum = (byte) (hash[0] & 0xf0);
|
byte checksum = (byte) (hash[0] & 0xf0);
|
||||||
message = Bytes.concat(message, new byte[] {
|
entropy = Bytes.concat(entropy, new byte[] {
|
||||||
checksum
|
checksum
|
||||||
});
|
});
|
||||||
|
|
||||||
return BIP39.encode(message, "en");
|
return BIP39.encode(entropy, "en");
|
||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/seedPhrase")
|
@Path("/mnemonic")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Calculate binary form of 12-word BIP39 seed phrase",
|
summary = "Calculate binary entropy from 12-word BIP39 mnemonic",
|
||||||
description = "Returns the base58-encoded binary form, or \"false\" if phrase is invalid.",
|
description = "Returns the base58-encoded binary form, or \"false\" if mnemonic is invalid.",
|
||||||
requestBody = @RequestBody(
|
requestBody = @RequestBody(
|
||||||
required = true,
|
required = true,
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@ -207,7 +243,7 @@ public class UtilsResource {
|
|||||||
),
|
),
|
||||||
responses = {
|
responses = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
description = "the private key",
|
description = "entropy in base58",
|
||||||
content = @Content(
|
content = @Content(
|
||||||
mediaType = MediaType.TEXT_PLAIN,
|
mediaType = MediaType.TEXT_PLAIN,
|
||||||
schema = @Schema(
|
schema = @Schema(
|
||||||
@ -217,25 +253,94 @@ public class UtilsResource {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
public String getBinarySeed(String seedPhrase) {
|
public String fromMnemonic(String mnemonic) {
|
||||||
if (seedPhrase.isEmpty())
|
if (mnemonic.isEmpty())
|
||||||
return "false";
|
return "false";
|
||||||
|
|
||||||
// Strip leading/trailing whitespace if any
|
// Strip leading/trailing whitespace if any
|
||||||
seedPhrase = seedPhrase.trim();
|
mnemonic = mnemonic.trim();
|
||||||
|
|
||||||
String[] phraseWords = seedPhrase.split(" ");
|
String[] phraseWords = mnemonic.split(" ");
|
||||||
if (phraseWords.length != 12)
|
if (phraseWords.length != 12)
|
||||||
return "false";
|
return "false";
|
||||||
|
|
||||||
// Convert BIP39 seed phrase to binary
|
// Convert BIP39 mnemonic to binary
|
||||||
byte[] binary = BIP39.decode(phraseWords, "en");
|
byte[] binary = BIP39.decode(phraseWords, "en");
|
||||||
if (binary == null)
|
if (binary == null)
|
||||||
return "false";
|
return "false";
|
||||||
|
|
||||||
byte[] message = Arrays.copyOf(binary, 16); // 132 bits is 16.5 bytes, but we're discarding checksum nybble
|
byte[] entropy = Arrays.copyOf(binary, 16); // 132 bits is 16.5 bytes, but we're discarding checksum nybble
|
||||||
|
|
||||||
return Base58.encode(message);
|
byte checksumNybble = (byte) (binary[16] & 0xf0);
|
||||||
|
byte[] checksum = Crypto.digest(entropy);
|
||||||
|
if ((checksum[0] & 0xf0) != checksumNybble)
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
public String privateKey(@PathParam("entropy") String entropy58) {
|
||||||
|
byte[] entropy;
|
||||||
|
try {
|
||||||
|
entropy = Base58.decode(entropy58);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entropy.length != 16)
|
||||||
|
throw ApiErrorFactory.getInstance().createError(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"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
public String publicKey(@PathParam("privateKey") String privateKey58) {
|
||||||
|
byte[] privateKey;
|
||||||
|
try {
|
||||||
|
privateKey = Base58.decode(privateKey58);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (privateKey.length != 32)
|
||||||
|
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
|
||||||
|
|
||||||
|
byte[] publicKey = new PrivateKeyAccount(null, privateKey).getPublicKey();
|
||||||
|
|
||||||
|
return Base58.encode(publicKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -9,7 +9,7 @@ public class BIP39 {
|
|||||||
|
|
||||||
private static final int BITS_PER_WORD = 11;
|
private static final int BITS_PER_WORD = 11;
|
||||||
|
|
||||||
/** Convert BIP39 seed phrase to binary form */
|
/** Convert BIP39 mnemonic to binary 'entropy' */
|
||||||
public static byte[] decode(String[] phraseWords, String lang) {
|
public static byte[] decode(String[] phraseWords, String lang) {
|
||||||
if (lang == null)
|
if (lang == null)
|
||||||
lang = "en";
|
lang = "en";
|
||||||
@ -18,7 +18,7 @@ public class BIP39 {
|
|||||||
if (wordList == null)
|
if (wordList == null)
|
||||||
throw new IllegalStateException("BIP39 word list for lang '" + lang + "' unavailable");
|
throw new IllegalStateException("BIP39 word list for lang '" + lang + "' unavailable");
|
||||||
|
|
||||||
byte[] output = new byte[(phraseWords.length * BITS_PER_WORD + 7) / 8];
|
byte[] entropy = new byte[(phraseWords.length * BITS_PER_WORD + 7) / 8];
|
||||||
int byteIndex = 0;
|
int byteIndex = 0;
|
||||||
int bitShift = 3;
|
int bitShift = 3;
|
||||||
|
|
||||||
@ -28,28 +28,28 @@ public class BIP39 {
|
|||||||
// Word not found
|
// Word not found
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
output[byteIndex++] |= (byte) (wordListIndex >> bitShift);
|
entropy[byteIndex++] |= (byte) (wordListIndex >> bitShift);
|
||||||
|
|
||||||
bitShift = 8 - bitShift;
|
bitShift = 8 - bitShift;
|
||||||
if (bitShift >= 0) {
|
if (bitShift >= 0) {
|
||||||
// Leftover fits inside one byte
|
// Leftover fits inside one byte
|
||||||
output[byteIndex] |= (byte) ((wordListIndex << bitShift));
|
entropy[byteIndex] |= (byte) ((wordListIndex << bitShift));
|
||||||
bitShift = BITS_PER_WORD - bitShift;
|
bitShift = BITS_PER_WORD - bitShift;
|
||||||
} else {
|
} else {
|
||||||
// Leftover spread over next two bytes
|
// Leftover spread over next two bytes
|
||||||
bitShift = 0 - bitShift;
|
bitShift = 0 - bitShift;
|
||||||
output[byteIndex++] |= (byte) (wordListIndex >> bitShift);
|
entropy[byteIndex++] |= (byte) (wordListIndex >> bitShift);
|
||||||
|
|
||||||
output[byteIndex] |= (byte) ((wordListIndex << (8 - bitShift)));
|
entropy[byteIndex] |= (byte) ((wordListIndex << (8 - bitShift)));
|
||||||
bitShift = bitShift + BITS_PER_WORD - 8;
|
bitShift = bitShift + BITS_PER_WORD - 8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
return entropy;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convert binary to BIP39 seed phrase */
|
/** Convert binary entropy to BIP39 mnemonic */
|
||||||
public static String encode(byte[] input, String lang) {
|
public static String encode(byte[] entropy, String lang) {
|
||||||
if (lang == null)
|
if (lang == null)
|
||||||
lang = "en";
|
lang = "en";
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ public class BIP39 {
|
|||||||
for (int bitCount = 0; bitCount < BITS_PER_WORD; ++bitCount) {
|
for (int bitCount = 0; bitCount < BITS_PER_WORD; ++bitCount) {
|
||||||
wordListIndex <<= 1;
|
wordListIndex <<= 1;
|
||||||
|
|
||||||
if ((input[byteIndex] & bitMask) != 0)
|
if ((entropy[byteIndex] & bitMask) != 0)
|
||||||
++wordListIndex;
|
++wordListIndex;
|
||||||
|
|
||||||
bitMask >>= 1;
|
bitMask >>= 1;
|
||||||
@ -74,7 +74,7 @@ public class BIP39 {
|
|||||||
bitMask = 128;
|
bitMask = 128;
|
||||||
++byteIndex;
|
++byteIndex;
|
||||||
|
|
||||||
if (byteIndex >= input.length)
|
if (byteIndex >= entropy.length)
|
||||||
return String.join(" ", phraseWords);
|
return String.join(" ", phraseWords);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user