API: terminology corrections and more utility calls

This commit is contained in:
catbref 2018-12-14 13:39:17 +00:00
parent e1dbaa5597
commit 68f99cfc11
2 changed files with 162 additions and 57 deletions

View File

@ -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);
} }
} }

View File

@ -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);
} }
} }