From 68f99cfc11def87de48f99ee68e2ada9d60c7e72 Mon Sep 17 00:00:00 2001 From: catbref Date: Fri, 14 Dec 2018 13:39:17 +0000 Subject: [PATCH] API: terminology corrections and more utility calls --- src/api/UtilsResource.java | 197 ++++++++++++++++++++++++++++--------- src/utils/BIP39.java | 22 ++--- 2 files changed, 162 insertions(+), 57 deletions(-) diff --git a/src/api/UtilsResource.java b/src/api/UtilsResource.java index 9016ea92..98cc72a8 100644 --- a/src/api/UtilsResource.java +++ b/src/api/UtilsResource.java @@ -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.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import qora.account.PrivateKeyAccount; import qora.crypto.Crypto; import utils.BIP39; import utils.Base58; @@ -13,23 +14,22 @@ import utils.Base58; import java.security.SecureRandom; import java.util.Arrays; import java.util.Base64; -import java.util.List; 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 globalization.BIP39WordList; - @Path("/utils") @Produces({ MediaType.TEXT_PLAIN @@ -43,9 +43,9 @@ public class UtilsResource { HttpServletRequest request; @POST - @Path("/base58from64") + @Path("/fromBase64") @Operation( - summary = "Convert base64 data to base58", + summary = "Convert base64 data to hex", requestBody = @RequestBody( required = true, content = @Content( @@ -57,7 +57,7 @@ public class UtilsResource { ), responses = { @ApiResponse( - description = "base58 data", + description = "hex string", content = @Content( schema = @Schema( type = "string" @@ -66,18 +66,18 @@ public class UtilsResource { ) } ) - public String base58from64(String base64) { + public String fromBase64(String base64) { try { - return Base58.encode(Base64.getDecoder().decode(base64.trim())); + return HashCode.fromBytes(Base64.getDecoder().decode(base64.trim())).toString(); } catch (IllegalArgumentException e) { throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA); } } @POST - @Path("/base64from58") + @Path("/fromBase58") @Operation( - summary = "Convert base58 data to base64", + summary = "Convert base58 data to hex", requestBody = @RequestBody( required = true, content = @Content( @@ -89,7 +89,7 @@ public class UtilsResource { ), responses = { @ApiResponse( - description = "base64 data", + description = "hex string", content = @Content( schema = @Schema( type = "string" @@ -100,17 +100,55 @@ public class UtilsResource { ) public String base64from58(String base58) { try { - return Base64.getEncoder().encodeToString(Base58.decode(base58.trim())); + return HashCode.fromBytes(Base58.decode(base58.trim())).toString(); } catch (NumberFormatException e) { throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA); } } @GET - @Path("/seed") + @Path("/toBase64/{hex}") @Operation( - summary = "Generate random seed", - description = "Optionally pass seed length, defaults to 32 bytes.", + 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", @@ -123,24 +161,24 @@ public class UtilsResource { ) } ) - public String seed(@QueryParam("length") Integer length) { + public String random(@QueryParam("length") Integer length) { if (length == null) length = 32; - byte[] seed = new byte[length]; - new SecureRandom().nextBytes(seed); - return Base58.encode(seed); + byte[] random = new byte[length]; + new SecureRandom().nextBytes(random); + return Base58.encode(random); } @GET - @Path("/seedPhrase") + @Path("/mnemonic") @Operation( - summary = "Generate random 12-word BIP39 seed phrase", - description = "Optionally pass 16-byte, base58-encoded entropy input or entropy will be internally generated.
" + summary = "Generate 12-word BIP39 mnemonic", + description = "Optionally pass 16-byte, base58-encoded entropy or entropy will be internally generated.
" + "Example entropy input: YcVfxkQb6JRzqk5kF2tNLv", responses = { @ApiResponse( - description = "seed phrase", + description = "mnemonic", content = @Content( mediaType = MediaType.TEXT_PLAIN, 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. * UUID (128bits) and another 4 bits gives 132 bits. * 132 bits, divided by 11, gives 12 words. */ - final int BITS_PER_WORD = 11; - - byte[] message; - if (input != null) { + byte[] entropy; + if (suppliedEntropy != null) { // Use caller-supplied entropy input try { - message = Base58.decode(input); + entropy = Base58.decode(suppliedEntropy); } catch (NumberFormatException e) { throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA); } // Must be 16-bytes - if (message.length != 16) + if (entropy.length != 16) throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA); } else { // Generate entropy internally @@ -176,26 +212,26 @@ public class UtilsResource { byte[] uuidMSB = Longs.toByteArray(uuid.getMostSignificantBits()); byte[] uuidLSB = Longs.toByteArray(uuid.getLeastSignificantBits()); - message = Bytes.concat(uuidMSB, uuidLSB); + entropy = Bytes.concat(uuidMSB, uuidLSB); } // 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). byte checksum = (byte) (hash[0] & 0xf0); - message = Bytes.concat(message, new byte[] { + entropy = Bytes.concat(entropy, new byte[] { checksum }); - return BIP39.encode(message, "en"); + return BIP39.encode(entropy, "en"); } @POST - @Path("/seedPhrase") + @Path("/mnemonic") @Operation( - summary = "Calculate binary form of 12-word BIP39 seed phrase", - description = "Returns the base58-encoded binary form, or \"false\" if phrase is invalid.", + 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( @@ -207,7 +243,7 @@ public class UtilsResource { ), responses = { @ApiResponse( - description = "the private key", + description = "entropy in base58", content = @Content( mediaType = MediaType.TEXT_PLAIN, schema = @Schema( @@ -217,25 +253,94 @@ public class UtilsResource { ) } ) - public String getBinarySeed(String seedPhrase) { - if (seedPhrase.isEmpty()) + public String fromMnemonic(String mnemonic) { + if (mnemonic.isEmpty()) return "false"; // Strip leading/trailing whitespace if any - seedPhrase = seedPhrase.trim(); + mnemonic = mnemonic.trim(); - String[] phraseWords = seedPhrase.split(" "); + String[] phraseWords = mnemonic.split(" "); if (phraseWords.length != 12) return "false"; - // Convert BIP39 seed phrase to binary + // Convert BIP39 mnemonic to binary byte[] binary = BIP39.decode(phraseWords, "en"); if (binary == null) 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); + } + +} \ No newline at end of file diff --git a/src/utils/BIP39.java b/src/utils/BIP39.java index 97ea7f17..6fbb8dfc 100644 --- a/src/utils/BIP39.java +++ b/src/utils/BIP39.java @@ -9,7 +9,7 @@ public class BIP39 { 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) { if (lang == null) lang = "en"; @@ -18,7 +18,7 @@ public class BIP39 { if (wordList == null) 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 bitShift = 3; @@ -28,28 +28,28 @@ public class BIP39 { // Word not found return null; - output[byteIndex++] |= (byte) (wordListIndex >> bitShift); + entropy[byteIndex++] |= (byte) (wordListIndex >> bitShift); bitShift = 8 - bitShift; if (bitShift >= 0) { // Leftover fits inside one byte - output[byteIndex] |= (byte) ((wordListIndex << bitShift)); + entropy[byteIndex] |= (byte) ((wordListIndex << bitShift)); bitShift = BITS_PER_WORD - bitShift; } else { // Leftover spread over next two bytes 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; } } - return output; + return entropy; } - /** Convert binary to BIP39 seed phrase */ - public static String encode(byte[] input, String lang) { + /** Convert binary entropy to BIP39 mnemonic */ + public static String encode(byte[] entropy, String lang) { if (lang == null) lang = "en"; @@ -66,7 +66,7 @@ public class BIP39 { for (int bitCount = 0; bitCount < BITS_PER_WORD; ++bitCount) { wordListIndex <<= 1; - if ((input[byteIndex] & bitMask) != 0) + if ((entropy[byteIndex] & bitMask) != 0) ++wordListIndex; bitMask >>= 1; @@ -74,7 +74,7 @@ public class BIP39 { bitMask = 128; ++byteIndex; - if (byteIndex >= input.length) + if (byteIndex >= entropy.length) return String.join(" ", phraseWords); } }