package org.ripple.power; import org.ripple.bouncycastle.asn1.ASN1InputStream; import org.ripple.bouncycastle.asn1.DERInteger; import org.ripple.bouncycastle.asn1.DERSequenceGenerator; import org.ripple.bouncycastle.asn1.DLSequence; import org.ripple.bouncycastle.asn1.sec.SECNamedCurves; import org.ripple.bouncycastle.asn1.x9.X9ECParameters; import org.ripple.bouncycastle.crypto.digests.RIPEMD160Digest; import org.ripple.bouncycastle.crypto.engines.AESEngine; import org.ripple.bouncycastle.crypto.generators.SCrypt; import org.ripple.bouncycastle.crypto.params.ECDomainParameters; import org.ripple.bouncycastle.crypto.params.ECPrivateKeyParameters; import org.ripple.bouncycastle.crypto.params.ECPublicKeyParameters; import org.ripple.bouncycastle.crypto.params.KeyParameter; import org.ripple.bouncycastle.crypto.params.ParametersWithRandom; import org.ripple.bouncycastle.crypto.signers.ECDSASigner; import org.ripple.bouncycastle.math.ec.ECPoint; import org.ripple.bouncycastle.util.Arrays; import org.ripple.power.utils.KeyPair; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; public final class CoinUtils { private static final ECDomainParameters EC_PARAMS; // only Bitcoin base58 private static final char[] BASE58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" .toCharArray(); public static final SecureRandom SECURE_RANDOM = new SecureRandom(); private static final BigInteger LARGEST_PRIVATE_KEY = new BigInteger( "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16); public static final long MIN_FEE_PER_KB = 10000; static { X9ECParameters params = SECNamedCurves.getByName("secp256k1"); EC_PARAMS = new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); } public static byte[] generatePublicKey(BigInteger privateKey, boolean compressed) { synchronized (EC_PARAMS) { ECPoint res = EC_PARAMS.getG().multiply(privateKey); return res.getEncoded(compressed); } } public static byte[] generateKey(BigInteger privateKey) { synchronized (EC_PARAMS) { ECPoint res = EC_PARAMS.getG().multiply(privateKey); return res.getEncoded(true); } } public static byte[] doubleSha256(byte[] bytes) { try { MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); return sha256.digest(sha256.digest(bytes)); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } public static String formatValue(double value) { if (value < 0) { throw new NumberFormatException("Negative value " + value); } String s = String.format("%.8f", value); while (s.length() > 1 && (s.endsWith("0") || s.endsWith("."))) { s = (s.substring(0, s.length() - 1)); } return s; } public static String formatValue(long value) throws NumberFormatException { if (value < 0) { throw new NumberFormatException("Negative value " + value); } StringBuilder sb = new StringBuilder(Long.toString(value)); while (sb.length() <= 8) { sb.insert(0, '0'); } sb.insert(sb.length() - 8, '.'); while (sb.length() > 1 && (sb.charAt(sb.length() - 1) == '0' || sb .charAt(sb.length() - 1) == '.')) { sb.setLength(sb.length() - 1); } return sb.toString(); } public static long parseValue(String valueStr) throws NumberFormatException { return (long) (Double.parseDouble(valueStr) * 1e8); } public static class PrivateKeyInfo { public static final int TYPE_WIF = 0; public static final int TYPE_MINI = 1; public static final int TYPE_BRAIN_WALLET = 2; public final int type; public final String privateKeyEncoded; public final BigInteger privateKeyDecoded; public final boolean isPublicKeyCompressed; public PrivateKeyInfo(int type, String privateKeyEncoded, BigInteger privateKeyDecoded, boolean isPublicKeyCompressed) { this.type = type; this.privateKeyEncoded = privateKeyEncoded; this.privateKeyDecoded = privateKeyDecoded; this.isPublicKeyCompressed = isPublicKeyCompressed; } } public static class Bip38PrivateKeyInfo extends PrivateKeyInfo { public static final int TYPE_BIP38 = 4; public final String confirmationCode; public final String password; public Bip38PrivateKeyInfo(String privateKeyEncoded, String confirmationCode, boolean isPublicKeyCompressed) { super(TYPE_BIP38, privateKeyEncoded, null, isPublicKeyCompressed); this.confirmationCode = confirmationCode; this.password = null; } public Bip38PrivateKeyInfo(String privateKeyEncoded, BigInteger privateKeyDecoded, String password, boolean isPublicKeyCompressed) { super(TYPE_BIP38, privateKeyEncoded, privateKeyDecoded, isPublicKeyCompressed); this.confirmationCode = null; this.password = password; } } public static PrivateKeyInfo decodePrivateKey(String encodedPrivateKey) { if (encodedPrivateKey.length() > 0) { try { byte[] decoded = decodeBase58(encodedPrivateKey); if (decoded != null && (decoded.length == 37 || decoded.length == 38) && (decoded[0] & 0xff) == 0x80) { if (verifyChecksum(decoded)) { byte[] secret = new byte[32]; System.arraycopy(decoded, 1, secret, 0, secret.length); boolean isPublicKeyCompressed; if (decoded.length == 38) { if (decoded[decoded.length - 5] == 1) { isPublicKeyCompressed = true; } else { return null; } } else { isPublicKeyCompressed = false; } BigInteger privateKeyBigInteger = new BigInteger(1, secret); if (privateKeyBigInteger.compareTo(BigInteger.ONE) > 0 && privateKeyBigInteger .compareTo(LARGEST_PRIVATE_KEY) < 0) { return new PrivateKeyInfo(PrivateKeyInfo.TYPE_WIF, encodedPrivateKey, privateKeyBigInteger, isPublicKeyCompressed); } } } else if (decoded != null && decoded.length == 43 && (decoded[0] & 0xff) == 0x01 && ((decoded[1] & 0xff) == 0x43 || (decoded[1] & 0xff) == 0x42)) { if (verifyChecksum(decoded)) { return new PrivateKeyInfo( Bip38PrivateKeyInfo.TYPE_BIP38, encodedPrivateKey, null, false); } } } catch (Exception ignored) { } } return decodePrivateKeyAsSHA256(encodedPrivateKey); } public static PrivateKeyInfo decodePrivateKeyAsSHA256( String encodedPrivateKey) { if (encodedPrivateKey.length() > 0) { try { MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); BigInteger privateKeyBigInteger = new BigInteger(1, sha256.digest(encodedPrivateKey.getBytes())); if (privateKeyBigInteger.compareTo(BigInteger.ONE) > 0 && privateKeyBigInteger.compareTo(LARGEST_PRIVATE_KEY) < 0) { int type; if (sha256.digest((encodedPrivateKey + '?') .getBytes("UTF-8"))[0] == 0) { type = PrivateKeyInfo.TYPE_MINI; } else { type = PrivateKeyInfo.TYPE_BRAIN_WALLET; } final boolean isPublicKeyCompressed = false; return new PrivateKeyInfo(type, encodedPrivateKey, privateKeyBigInteger, isPublicKeyCompressed); } } catch (Exception ignored) { } } return null; } public static boolean verifyBitcoinAddress(String address) { byte[] decodedAddress = decodeBase58(address); return !(decodedAddress == null || decodedAddress.length < 6 || decodedAddress[0] != 0 || !verifyChecksum(decodedAddress)); } public static boolean verifyChecksum(byte[] bytesWithChecksumm) { try { if (bytesWithChecksumm == null || bytesWithChecksumm.length < 5) { return false; } MessageDigest digestSha = MessageDigest.getInstance("SHA-256"); digestSha.update(bytesWithChecksumm, 0, bytesWithChecksumm.length - 4); byte[] first = digestSha.digest(); byte[] calculatedDigest = digestSha.digest(first); boolean checksumValid = true; for (int i = 0; i < 4; i++) { if (calculatedDigest[i] != bytesWithChecksumm[bytesWithChecksumm.length - 4 + i]) { checksumValid = false; } } return checksumValid; } catch (Exception e) { throw new RuntimeException(e); } } public static byte[] sha256ripemd160(byte[] publicKey) { try { MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); byte[] sha256hash = sha256.digest(publicKey); RIPEMD160Digest ripemd160Digest = new RIPEMD160Digest(); ripemd160Digest.update(sha256hash, 0, sha256hash.length); byte[] hashedPublicKey = new byte[20]; ripemd160Digest.doFinal(hashedPublicKey, 0); return hashedPublicKey; } catch (Exception e) { throw new RuntimeException(e); } } public static String publicKeyToAddress(byte[] publicKey) { try { byte[] hashedPublicKey = sha256ripemd160(publicKey); int size = hashedPublicKey.length; byte[] addressBytes = new byte[1 + size + 4]; addressBytes[0] = 0; System.arraycopy(hashedPublicKey, 0, addressBytes, 1, size); MessageDigest digestSha = MessageDigest.getInstance("SHA-256"); digestSha.update(addressBytes, 0, addressBytes.length - 4); byte[] check = digestSha.digest(digestSha.digest()); System.arraycopy(check, 0, addressBytes, hashedPublicKey.length + 1, 4); return CoinUtils.encodeBase58(addressBytes); } catch (Exception e) { return ""; } } private static final int BASE58_CHUNK_DIGITS = 10; private static final BigInteger BASE58_CHUNK_MOD = BigInteger .valueOf(0x5fa8624c7fba400L); private static final byte[] BASE58_VALUES = new byte[] { -1, -1, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, -1, -1, -1, -1, -1, -1, 9, 10, 11, 12, 13, 14, 15, 16, -1, 17, 18, 19, 20, 21, -1, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, -1, -1, -1, -1, -1, -1, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, -1, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }; public static byte[] decodeBase58(String input) { if (input == null) { return null; } input = input.trim(); if (input.length() == 0) { return new byte[0]; } BigInteger resultNum = BigInteger.ZERO; int nLeadingZeros = 0; while (nLeadingZeros < input.length() && input.charAt(nLeadingZeros) == BASE58[0]) { nLeadingZeros++; } long acc = 0; int nDigits = 0; int p = nLeadingZeros; while (p < input.length()) { int v = BASE58_VALUES[input.charAt(p) & 0xff]; if (v >= 0) { acc *= 58; acc += v; nDigits++; if (nDigits == BASE58_CHUNK_DIGITS) { resultNum = resultNum.multiply(BASE58_CHUNK_MOD).add( BigInteger.valueOf(acc)); acc = 0; nDigits = 0; } p++; } else { break; } } if (nDigits > 0) { long mul = 58; while (--nDigits > 0) { mul *= 58; } resultNum = resultNum.multiply(BigInteger.valueOf(mul)).add( BigInteger.valueOf(acc)); } final int BASE58_SPACE = -2; while (p < input.length() && BASE58_VALUES[input.charAt(p) & 0xff] == BASE58_SPACE) { p++; } if (p < input.length()) { return null; } byte[] plainNumber = resultNum.toByteArray(); int plainNumbersOffs = plainNumber[0] == 0 ? 1 : 0; byte[] result = new byte[nLeadingZeros + plainNumber.length - plainNumbersOffs]; System.arraycopy(plainNumber, plainNumbersOffs, result, nLeadingZeros, plainNumber.length - plainNumbersOffs); return result; } public static String encodeBase58(byte[] input) { if (input == null) { return null; } StringBuilder str = new StringBuilder((input.length * 350) / 256 + 1); BigInteger bn = new BigInteger(1, input); long rem; while (true) { BigInteger[] divideAndRemainder = bn .divideAndRemainder(BASE58_CHUNK_MOD); bn = divideAndRemainder[0]; rem = divideAndRemainder[1].longValue(); if (bn.compareTo(BigInteger.ZERO) == 0) { break; } for (int i = 0; i < BASE58_CHUNK_DIGITS; i++) { str.append(BASE58[(int) (rem % 58)]); rem /= 58; } } while (rem != 0) { str.append(BASE58[(int) (rem % 58)]); rem /= 58; } str.reverse(); int nLeadingZeros = 0; while (nLeadingZeros < input.length && input[nLeadingZeros] == 0) { str.insert(0, BASE58[0]); nLeadingZeros++; } return str.toString(); } final static char[] hexArray = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; public static String toHex(byte[] bytes) { if (bytes == null) { return ""; } char[] hexChars = new char[bytes.length * 2]; int v; for (int j = 0; j < bytes.length; j++) { v = bytes[j] & 0xFF; hexChars[j * 2] = hexArray[v >>> 4]; hexChars[j * 2 + 1] = hexArray[v & 0x0F]; } return new String(hexChars); } public static byte[] fromHex(String s) { if (s != null) { try { StringBuilder sb = new StringBuilder(s.length()); for (int i = 0; i < s.length(); i++) { char ch = s.charAt(i); if (!Character.isWhitespace(ch)) { sb.append(ch); } } s = sb.toString(); int len = s.length(); byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { int hi = (Character.digit(s.charAt(i), 16) << 4); int low = Character.digit(s.charAt(i + 1), 16); if (hi >= 256 || low < 0 || low >= 16) { return null; } data[i / 2] = (byte) (hi | low); } return data; } catch (Exception ignored) { } } return null; } public static byte[] sign(BigInteger privateKey, byte[] input) { synchronized (EC_PARAMS) { ECDSASigner signer = new ECDSASigner(); ECPrivateKeyParameters privateKeyParam = new ECPrivateKeyParameters( privateKey, EC_PARAMS); signer.init(true, new ParametersWithRandom(privateKeyParam, SECURE_RANDOM)); BigInteger[] sign = signer.generateSignature(input); try { ByteArrayOutputStream baos = new ByteArrayOutputStream(72); DERSequenceGenerator derGen = new DERSequenceGenerator(baos); derGen.addObject(new DERInteger(sign[0])); derGen.addObject(new DERInteger(sign[1])); derGen.close(); return baos.toByteArray(); } catch (IOException e) { throw new RuntimeException(e); } } } public static boolean verify(byte[] publicKey, byte[] signature, byte[] msg) { synchronized (EC_PARAMS) { boolean valid; ECDSASigner signerVer = new ECDSASigner(); try { ECPublicKeyParameters pubKey = new ECPublicKeyParameters( EC_PARAMS.getCurve().decodePoint(publicKey), EC_PARAMS); signerVer.init(false, pubKey); ASN1InputStream derSigStream = new ASN1InputStream(signature); DLSequence seq = (DLSequence) derSigStream.readObject(); BigInteger r = ((DERInteger) seq.getObjectAt(0)) .getPositiveValue(); BigInteger s = ((DERInteger) seq.getObjectAt(1)) .getPositiveValue(); derSigStream.close(); valid = signerVer.verifySignature(msg, r, s); } catch (Exception e) { throw new RuntimeException(); } return valid; } } public static byte[] reverse(byte[] bytes) { byte[] result = new byte[bytes.length]; for (int i = 0; i < bytes.length; i++) { result[i] = bytes[bytes.length - i - 1]; } return result; } public static byte[] reverseInPlace(byte[] bytes) { int len = bytes.length / 2; for (int i = 0; i < len; i++) { byte t = bytes[i]; bytes[i] = bytes[bytes.length - i - 1]; bytes[bytes.length - i - 1] = t; } return bytes; } public static byte[] bigIntegerToBytes(BigInteger b, int numBytes) { if (b == null) { return null; } byte[] bytes = new byte[numBytes]; byte[] biBytes = b.toByteArray(); int start = (biBytes.length == numBytes + 1) ? 1 : 0; int length = Math.min(biBytes.length, numBytes); System.arraycopy(biBytes, start, bytes, numBytes - length, length); return bytes; } public static String bip38GetIntermediateCode(String password) throws InterruptedException { try { byte[] ownerSalt = new byte[8]; SECURE_RANDOM.nextBytes(ownerSalt); byte[] passFactor = SCrypt.generate(password.getBytes("UTF-8"), ownerSalt, 16384, 8, 8, 32); ECPoint uncompressed = EC_PARAMS.getG().multiply( new BigInteger(1, passFactor)); byte[] passPoint = new ECPoint.Fp(EC_PARAMS.getCurve(), uncompressed.getX(), uncompressed.getY(), true) .getEncoded(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fromHex("2CE9B3E1FF39E253")); baos.write(ownerSalt); baos.write(passPoint); baos.write(doubleSha256(baos.toByteArray()), 0, 4); return encodeBase58(baos.toByteArray()); } catch (IOException e) { throw new RuntimeException(e); } } public static KeyPair bip38GenerateKeyPair(String intermediateCode, boolean compressedPublicKey) throws InterruptedException, Exception { byte[] intermediateBytes = decodeBase58(intermediateCode); if (!verifyChecksum(intermediateBytes) || intermediateBytes.length != 53) { throw new RuntimeException("Bad intermediate code"); } byte[] magic = fromHex("2CE9B3E1FF39E2"); for (int i = 0; i < magic.length; i++) { if (magic[i] != intermediateBytes[i]) { throw new Exception("It isn't an intermediate code"); } } try { byte[] ownerEntropy = new byte[8]; System.arraycopy(intermediateBytes, 8, ownerEntropy, 0, 8); byte[] passPoint = new byte[33]; System.arraycopy(intermediateBytes, 16, passPoint, 0, 33); byte flag = (byte) (compressedPublicKey ? 0x20 : 0x00);// compressed // public // key byte[] seedB = new byte[24]; SECURE_RANDOM.nextBytes(seedB); byte[] factorB = doubleSha256(seedB); BigInteger factorBInteger = new BigInteger(1, factorB); ECPoint uncompressedPublicKeyPoint = EC_PARAMS.getCurve() .decodePoint(passPoint).multiply(factorBInteger); String address; byte[] publicKey; if (compressedPublicKey) { publicKey = new ECPoint.Fp(EC_PARAMS.getCurve(), uncompressedPublicKeyPoint.getX(), uncompressedPublicKeyPoint.getY(), true).getEncoded(); address = publicKeyToAddress(publicKey); } else { publicKey = uncompressedPublicKeyPoint.getEncoded(); address = publicKeyToAddress(publicKey); } byte[] addressHashAndOwnerSalt = new byte[12]; byte[] addressHash = new byte[4]; System.arraycopy(doubleSha256(address.getBytes("UTF-8")), 0, addressHash, 0, 4); System.arraycopy(addressHash, 0, addressHashAndOwnerSalt, 0, 4); System.arraycopy(ownerEntropy, 0, addressHashAndOwnerSalt, 4, 8); byte[] derived = SCrypt.generate(passPoint, addressHashAndOwnerSalt, 1024, 1, 1, 64); byte[] key = new byte[32]; System.arraycopy(derived, 32, key, 0, 32); for (int i = 0; i < 16; i++) { seedB[i] ^= derived[i]; } AESEngine cipher = new AESEngine(); cipher.init(true, new KeyParameter(key)); byte[] encryptedHalf1 = new byte[16]; byte[] encryptedHalf2 = new byte[16]; cipher.processBlock(seedB, 0, encryptedHalf1, 0); byte[] secondBlock = new byte[16]; System.arraycopy(encryptedHalf1, 8, secondBlock, 0, 8); System.arraycopy(seedB, 16, secondBlock, 8, 8); for (int i = 0; i < 16; i++) { secondBlock[i] ^= derived[i + 16]; } cipher.processBlock(secondBlock, 0, encryptedHalf2, 0); ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(0x01); baos.write(0x43); baos.write(flag); baos.write(addressHashAndOwnerSalt); baos.write(encryptedHalf1, 0, 8); baos.write(encryptedHalf2); baos.write(doubleSha256(baos.toByteArray()), 0, 4); String encryptedPrivateKey = encodeBase58(baos.toByteArray()); byte[] pointB = generatePublicKey(factorBInteger, true); byte pointBPrefix = (byte) (pointB[0] ^ (derived[63] & 0x01)); byte[] encryptedPointB = new byte[33]; encryptedPointB[0] = pointBPrefix; for (int i = 0; i < 32; i++) { pointB[i + 1] ^= derived[i]; } cipher.processBlock(pointB, 1, encryptedPointB, 1); cipher.processBlock(pointB, 17, encryptedPointB, 17); baos.reset(); baos.write(0x64); baos.write(0x3B); baos.write(0xF6); baos.write(0xA8); baos.write(0x9A); baos.write(flag); baos.write(addressHashAndOwnerSalt); baos.write(encryptedPointB); baos.write(doubleSha256(baos.toByteArray()), 0, 4); String confirmationCode = encodeBase58(baos.toByteArray()); Bip38PrivateKeyInfo privateKeyInfo = new Bip38PrivateKeyInfo( encryptedPrivateKey, confirmationCode, compressedPublicKey); return new KeyPair(address, publicKey, privateKeyInfo); } catch (IOException e) { throw new RuntimeException(e); } } public static String bip38DecryptConfirmation(String confirmationCode, String password) throws Exception { byte[] confirmationBytes = decodeBase58(confirmationCode); if (!verifyChecksum(confirmationBytes) || confirmationBytes.length != 55) { throw new RuntimeException("Bad confirmation code"); } byte[] magic = fromHex("643BF6A89A"); for (int i = 0; i < magic.length; i++) { if (magic[i] != confirmationBytes[i]) { throw new Exception("It isn't a confirmation code"); } } try { byte flag = confirmationBytes[5]; boolean compressed = (flag & 0x20) == 0x20; boolean lotSequencePresent = (flag & 0x04) == 0x04; byte[] addressHash = new byte[4]; System.arraycopy(confirmationBytes, 6, addressHash, 0, 4); byte[] ownerEntropy = new byte[8]; System.arraycopy(confirmationBytes, 10, ownerEntropy, 0, 8); byte[] salt = new byte[lotSequencePresent ? 4 : 8]; System.arraycopy(ownerEntropy, 0, salt, 0, salt.length); byte[] encryptedPointB = new byte[33]; System.arraycopy(confirmationBytes, 18, encryptedPointB, 0, 33); byte[] passFactor = SCrypt.generate(password.getBytes("UTF-8"), salt, 16384, 8, 8, 32); ECPoint uncompressed = EC_PARAMS.getG().multiply( new BigInteger(1, passFactor)); byte[] passPoint = new ECPoint.Fp(EC_PARAMS.getCurve(), uncompressed.getX(), uncompressed.getY(), true) .getEncoded(); byte[] addressHashAndOwnerSalt = new byte[12]; System.arraycopy(addressHash, 0, addressHashAndOwnerSalt, 0, 4); System.arraycopy(ownerEntropy, 0, addressHashAndOwnerSalt, 4, 8); byte[] derived = SCrypt.generate(passPoint, addressHashAndOwnerSalt, 1024, 1, 1, 64); byte[] key = new byte[32]; System.arraycopy(derived, 32, key, 0, 32); AESEngine cipher = new AESEngine(); cipher.init(false, new KeyParameter(key)); byte[] pointB = new byte[33]; pointB[0] = (byte) (encryptedPointB[0] ^ (derived[63] & 0x01)); cipher.processBlock(encryptedPointB, 1, pointB, 1); cipher.processBlock(encryptedPointB, 17, pointB, 17); for (int i = 0; i < 32; i++) { pointB[i + 1] ^= derived[i]; } ECPoint uncompressedPublicKey; try { uncompressedPublicKey = EC_PARAMS.getCurve() .decodePoint(pointB) .multiply(new BigInteger(1, passFactor)); } catch (RuntimeException e) { // point b doesn't belong the curve - bad password return null; } String address; if (compressed) { byte[] publicKey = new ECPoint.Fp(EC_PARAMS.getCurve(), uncompressedPublicKey.getX(), uncompressedPublicKey.getY(), true).getEncoded(); address = CoinUtils.publicKeyToAddress(publicKey); } else { address = CoinUtils.publicKeyToAddress(uncompressedPublicKey .getEncoded()); } byte[] decodedAddressHash = doubleSha256(address.getBytes("UTF-8")); for (int i = 0; i < 4; i++) { if (addressHash[i] != decodedAddressHash[i]) { return null; } } return address; } catch (Exception e) { throw new RuntimeException(e); } } public static String bip38Encrypt(KeyPair keyPair, String password) { try { byte[] addressHash = new byte[4]; System.arraycopy(doubleSha256(keyPair.address.getBytes("UTF-8")), 0, addressHash, 0, 4); byte[] passwordDerived = SCrypt.generate( password.getBytes("UTF-8"), addressHash, 16384, 8, 8, 64); byte[] xor = new byte[32]; System.arraycopy(passwordDerived, 0, xor, 0, 32); byte[] key = new byte[32]; System.arraycopy(passwordDerived, 32, key, 0, 32); byte[] privateKeyBytes = getPrivateKeyBytes(keyPair.privateKey.privateKeyDecoded); for (int i = 0; i < 32; i++) { xor[i] ^= privateKeyBytes[i]; } AESEngine cipher = new AESEngine(); cipher.init(true, new KeyParameter(key)); byte[] encryptedHalf1 = new byte[16]; byte[] encryptedHalf2 = new byte[16]; cipher.processBlock(xor, 0, encryptedHalf1, 0); cipher.processBlock(xor, 16, encryptedHalf2, 0); byte[] result = new byte[43]; result[0] = 1; result[1] = 0x42; result[2] = (byte) (keyPair.privateKey.isPublicKeyCompressed ? 0xe0 : 0xc0); System.arraycopy(addressHash, 0, result, 3, 4); System.arraycopy(encryptedHalf1, 0, result, 7, 16); System.arraycopy(encryptedHalf2, 0, result, 23, 16); MessageDigest digestSha = MessageDigest.getInstance("SHA-256"); digestSha.update(result, 0, result.length - 4); System.arraycopy(digestSha.digest(digestSha.digest()), 0, result, 39, 4); return encodeBase58(result); } catch (Exception e) { throw new RuntimeException(e); } } public static byte[] getPrivateKeyBytes(BigInteger privateKey) { byte[] privateKeyPlainNumber = privateKey.toByteArray(); int plainNumbersOffs = privateKeyPlainNumber[0] == 0 ? 1 : 0; byte[] privateKeyBytes = new byte[32]; System.arraycopy(privateKeyPlainNumber, plainNumbersOffs, privateKeyBytes, privateKeyBytes.length - (privateKeyPlainNumber.length - plainNumbersOffs), privateKeyPlainNumber.length - plainNumbersOffs); return privateKeyBytes; } public static KeyPair bip38Decrypt(String encryptedPrivateKey, String password) throws InterruptedException, Exception { byte[] encryptedPrivateKeyBytes = decodeBase58(encryptedPrivateKey); if (encryptedPrivateKeyBytes != null && encryptedPrivateKey.startsWith("6P") && verifyChecksum(encryptedPrivateKeyBytes) && encryptedPrivateKeyBytes[0] == 1) { try { byte[] addressHash = new byte[4]; System.arraycopy(encryptedPrivateKeyBytes, 3, addressHash, 0, 4); boolean compressed = (encryptedPrivateKeyBytes[2] & 0x20) == 0x20; AESEngine cipher = new AESEngine(); if (encryptedPrivateKeyBytes[1] == 0x42) { byte[] encryptedSecret = new byte[32]; System.arraycopy(encryptedPrivateKeyBytes, 7, encryptedSecret, 0, 32); byte[] passwordDerived = SCrypt.generate( password.getBytes("UTF-8"), addressHash, 16384, 8, 8, 64); byte[] key = new byte[32]; System.arraycopy(passwordDerived, 32, key, 0, 32); cipher.init(false, new KeyParameter(key)); byte[] secret = new byte[32]; cipher.processBlock(encryptedSecret, 0, secret, 0); cipher.processBlock(encryptedSecret, 16, secret, 16); for (int i = 0; i < 32; i++) { secret[i] ^= passwordDerived[i]; } KeyPair keyPair = new KeyPair(new Bip38PrivateKeyInfo( encryptedPrivateKey, new BigInteger(1, secret), password, compressed)); byte[] addressHashCalculated = new byte[4]; System.arraycopy( doubleSha256(keyPair.address.getBytes("UTF-8")), 0, addressHashCalculated, 0, 4); if (!Arrays.areEqual( addressHashCalculated, addressHash)) { throw new RuntimeException("Bad password"); } return keyPair; } else if (encryptedPrivateKeyBytes[1] == 0x43) { byte[] ownerSalt = new byte[8]; System.arraycopy(encryptedPrivateKeyBytes, 7, ownerSalt, 0, 8); byte[] passFactor = SCrypt.generate( password.getBytes("UTF-8"), ownerSalt, 16384, 8, 8, 32); ECPoint uncompressed = EC_PARAMS.getG().multiply( new BigInteger(1, passFactor)); byte[] passPoint = new ECPoint.Fp(EC_PARAMS.getCurve(), uncompressed.getX(), uncompressed.getY(), true) .getEncoded(); byte[] addressHashAndOwnerSalt = new byte[12]; System.arraycopy(encryptedPrivateKeyBytes, 3, addressHashAndOwnerSalt, 0, 12); byte[] derived = SCrypt.generate(passPoint, addressHashAndOwnerSalt, 1024, 1, 1, 64); byte[] key = new byte[32]; System.arraycopy(derived, 32, key, 0, 32); cipher.init(false, new KeyParameter(key)); byte[] decryptedHalf2 = new byte[16]; cipher.processBlock(encryptedPrivateKeyBytes, 23, decryptedHalf2, 0); for (int i = 0; i < 16; i++) { decryptedHalf2[i] ^= derived[i + 16]; } byte[] encryptedHalf1 = new byte[16]; System.arraycopy(encryptedPrivateKeyBytes, 15, encryptedHalf1, 0, 8); System.arraycopy(decryptedHalf2, 0, encryptedHalf1, 8, 8); byte[] decryptedHalf1 = new byte[16]; cipher.processBlock(encryptedHalf1, 0, decryptedHalf1, 0); for (int i = 0; i < 16; i++) { decryptedHalf1[i] ^= derived[i]; } byte[] seedB = new byte[24]; System.arraycopy(decryptedHalf1, 0, seedB, 0, 16); System.arraycopy(decryptedHalf2, 8, seedB, 16, 8); byte[] factorB = doubleSha256(seedB); BigInteger privateKey = new BigInteger(1, passFactor) .multiply(new BigInteger(1, factorB)).remainder( EC_PARAMS.getN()); KeyPair keyPair = new KeyPair(new Bip38PrivateKeyInfo( encryptedPrivateKey, privateKey, password, compressed)); byte[] resultedAddressHash = doubleSha256(keyPair.address .getBytes("UTF-8")); for (int i = 0; i < 4; i++) { if (addressHashAndOwnerSalt[i] != resultedAddressHash[i]) { throw new Exception("Bad password"); } } return keyPair; } else { throw new Exception("Bad encrypted private key"); } } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } else { throw new Exception("It is not an encrypted private key"); } } }