diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c9d47c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build +.gradle +.idea +*.iml diff --git a/build.gradle b/build.gradle index fb9bacd..e61be46 100644 --- a/build.gradle +++ b/build.gradle @@ -1,19 +1,23 @@ apply plugin: 'java' repositories { - mavenCentral() - maven { - url "http://distribution.bitcoinj.googlecode.com/git/releases/" - } + mavenCentral() + mavenLocal() } +allprojects { + apply plugin: 'maven' + group = 'com.fruitcat.bitcoin' + version = '0.1-SNAPSHOT' +} dependencies { compile 'com.madgag:scprov-jdk15on:1.47.0.3' compile 'com.google.guava:guava:15.0' - compile 'com.google:bitcoinj:0.10.3' + compile 'com.google:bitcoinj:0.11-SNAPSHOT' compile 'org.bouncycastle:bcprov-jdk16:1.46' - testCompile 'org.testng:testng:6.8.7' + testCompile 'org.testng:testng:6.8.7' + testCompile 'org.slf4j:slf4j-simple:1.6.1' } test { diff --git a/src/main/java/com/fruitcat/bitcoin/BIP38.java b/src/main/java/com/fruitcat/bitcoin/BIP38.java index 0515a00..7cec286 100644 --- a/src/main/java/com/fruitcat/bitcoin/BIP38.java +++ b/src/main/java/com/fruitcat/bitcoin/BIP38.java @@ -21,7 +21,12 @@ package com.fruitcat.bitcoin; -import com.google.bitcoin.core.*; +import com.google.bitcoin.core.Address; +import com.google.bitcoin.core.AddressFormatException; +import com.google.bitcoin.core.Base58; +import com.google.bitcoin.core.DumpedPrivateKey; +import com.google.bitcoin.core.ECKey; +import com.google.bitcoin.core.NetworkParameters; import com.google.bitcoin.params.MainNetParams; import com.lambdaworks.crypto.SCrypt; import org.bouncycastle.asn1.sec.SECNamedCurves; diff --git a/src/main/java/com/fruitcat/bitcoin/Utils.java b/src/main/java/com/fruitcat/bitcoin/Utils.java index 60ad289..911e43a 100644 --- a/src/main/java/com/fruitcat/bitcoin/Utils.java +++ b/src/main/java/com/fruitcat/bitcoin/Utils.java @@ -20,10 +20,19 @@ import com.google.bitcoin.core.AddressFormatException; import com.google.bitcoin.core.Base58; import org.bouncycastle.math.ec.ECPoint; +import org.spongycastle.crypto.BufferedBlockCipher; +import org.spongycastle.crypto.DataLengthException; +import org.spongycastle.crypto.InvalidCipherTextException; +import org.spongycastle.crypto.engines.AESFastEngine; +import org.spongycastle.crypto.modes.CBCBlockCipher; +import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.spongycastle.crypto.params.KeyParameter; +import org.spongycastle.crypto.params.ParametersWithIV; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.security.GeneralSecurityException; @@ -84,30 +93,65 @@ public static String base58Check(byte [] b) throws NoSuchAlgorithmException { /** * Encrypts plaintext with AES - * @param plaintext + * @param plainTextAsBytes * @param key * @return * @throws GeneralSecurityException */ - public static byte[] AESEncrypt(byte[] plaintext, byte[] key) throws GeneralSecurityException { - Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding", "BC"); - Key aesKey = new SecretKeySpec(key, "AES"); - cipher.init(Cipher.ENCRYPT_MODE, aesKey); - return cipher.doFinal(plaintext); + public static byte[] AESEncrypt(byte[] plainTextAsBytes, byte[] key) throws GeneralSecurityException { + try + { + final BufferedBlockCipher cipher = new BufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); + KeyParameter aesKey = new KeyParameter(key); + + cipher.init(true, aesKey); + + final byte[] encryptedBytes = new byte[cipher.getOutputSize(plainTextAsBytes.length)]; + final int length = cipher.processBytes(plainTextAsBytes, 0, plainTextAsBytes.length, encryptedBytes, 0); + + cipher.doFinal(encryptedBytes, length); + + return encryptedBytes; + } + catch (final InvalidCipherTextException x) + { + throw new GeneralSecurityException("Could not encrypt bytes", x); + } + catch (final DataLengthException x) + { + throw new GeneralSecurityException("Could not encrypt bytes", x); + } } /** * Decrypts ciphertext with AES - * @param ciphertext + * @param cipherBytes * @param key * @return * @throws GeneralSecurityException */ - public static byte[] AESDecrypt(byte[] ciphertext, byte[] key) throws GeneralSecurityException { - Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding", "BC"); - Key aesKey = new SecretKeySpec(key, "AES"); - cipher.init(Cipher.DECRYPT_MODE, aesKey); - return cipher.doFinal(ciphertext); + public static byte[] AESDecrypt(byte[] cipherBytes, byte[] key) throws GeneralSecurityException { + try + { + final BufferedBlockCipher cipher = new BufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); + KeyParameter aesKey = new KeyParameter(key); + cipher.init(false, aesKey); + + final byte[] decryptedBytes = new byte[cipher.getOutputSize(cipherBytes.length)]; + final int length = cipher.processBytes(cipherBytes, 0, cipherBytes.length, decryptedBytes, 0); + + cipher.doFinal(decryptedBytes, length); + + return decryptedBytes; + } + catch (final InvalidCipherTextException x) + { + throw new GeneralSecurityException("Could not decrypt input string", x); + } + catch (final DataLengthException x) + { + throw new GeneralSecurityException("Could not decrypt input string", x); + } } /** diff --git a/src/test/java/com/fruitcat/bitcoin/BIP38Test.java b/src/test/java/com/fruitcat/bitcoin/BIP38Test.java index 917c501..1487d51 100644 --- a/src/test/java/com/fruitcat/bitcoin/BIP38Test.java +++ b/src/test/java/com/fruitcat/bitcoin/BIP38Test.java @@ -19,6 +19,7 @@ public class BIP38Test { @Test public void noCompressionNoECMultiply() throws Exception { + BIP38.setNetParams(com.google.bitcoin.params.MainNetParams.get()); //test 1 String encryptedKey = "6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg"; String key = "5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR"; @@ -33,6 +34,7 @@ public void noCompressionNoECMultiply() throws Exception { @Test public void compressionNoECMultiply() throws Exception { + BIP38.setNetParams(com.google.bitcoin.params.MainNetParams.get()); //test 1 String encryptedKey = "6PYNKZ1EAgYgmQfmNVamxyXVWHzK5s6DGhwP4J5o44cvXdoY7sRzhtpUeo"; String key = "L44B5gGEpqEDRS9vVPz7QT35jcBG2r3CZwSwQ4fCewXAhAhqGVpP"; @@ -48,6 +50,7 @@ public void compressionNoECMultiply() throws Exception { //EC multiply, no compression, no lot/sequence numbers @Test public void ecMultiplyNoCompressionNoLot() throws Exception { + BIP38.setNetParams(com.google.bitcoin.params.MainNetParams.get()); //test 1 String encryptedKey = "6PfQu77ygVyJLZjfvMLyhLMQbYnu5uguoJJ4kMCLqWwPEdfpwANVS76gTX"; String key = "5K4caxezwjGCGfnoPTZ8tMcJBLB7Jvyjv4xxeacadhq8nLisLR2"; @@ -70,6 +73,7 @@ public void ecMultiplyNoCompressionNoLot() throws Exception { //EC multiply, no compression, lot/sequence @Test public void ecMultiplyNoCompressionLot() throws Exception { + BIP38.setNetParams(com.google.bitcoin.params.MainNetParams.get()); //test 1 String encryptedKey = "6PgNBNNzDkKdhkT6uJntUXwwzQV8Rr2tZcbkDcuC9DZRsS6AtHts4Ypo1j"; String key = "5JLdxTtcTHcfYcmJsNVy1v2PMDx432JPoYcBTVVRHpPaxUrdtf8"; @@ -92,6 +96,7 @@ public void ecMultiplyNoCompressionLot() throws Exception { //round encrypt and decrypt with a random ascii password @Test public void randomRoundTripNoEC() throws Exception { + BIP38.setNetParams(com.google.bitcoin.params.MainNetParams.get()); byte[] r = new byte[16]; (new Random()).nextBytes(r); String randomPass = new String(r, "ASCII"); @@ -103,6 +108,7 @@ public void randomRoundTripNoEC() throws Exception { //generate an encrypted key and make sure it looks ok. @Test public void generateEncryptedKey() throws Exception { + BIP38.setNetParams(com.google.bitcoin.params.MainNetParams.get()); String k = BIP38.generateEncryptedKey(testPass); String dk = BIP38.decrypt(testPass, k); assertEquals(dk.charAt(0), '5'); @@ -111,9 +117,32 @@ public void generateEncryptedKey() throws Exception { //check confirmation code @Test public void checkConfirmation() throws Exception { + BIP38.setNetParams(com.google.bitcoin.params.MainNetParams.get()); byte[] intermediate = Arrays.copyOfRange(Base58.decode(BIP38.intermediatePassphrase(testPass, -1, -1)), 0, 53); GeneratedKey gk = BIP38.encryptedKeyFromIntermediate(intermediate); assert(BIP38.verify(testPass, gk)); assert(!BIP38.verify("garbage", gk)); } + + @Test + public void litecoinRandomRoundTripNoEC() throws Exception { + BIP38.setNetParams(org.litecoin.LitecoinParams.get()); + byte[] r = new byte[16]; + (new Random()).nextBytes(r); + String randomPass = new String(r, "ASCII"); + String key = "6uRPj6B3bPCqf9KZLzc1VWHsmZgXXhx5qdm69BV9hg454LNwGwX"; + String encryptedKey = BIP38.encryptNoEC(randomPass, key, false); + assertEquals(key, (BIP38.decrypt(randomPass, encryptedKey))); + } + + @Test + public void litecoinCompressionNoECMultiply() throws Exception { + BIP38.setNetParams(org.litecoin.LitecoinParams.get()); + // 6PfMZr9CPt4JvcEZnSRRD8F9a2dYJ3H999sLoLJD3Lxh5gF5gC64TY8j1Q + "Satoshi" + // == 6vHEw38JPP6fvNBxVUeb499S7JBfRX5dMGadWrjv3w78UHPD2ac + String encryptedKey = "6PfMZr9CPt4JvcEZnSRRD8F9a2dYJ3H999sLoLJD3Lxh5gF5gC64TY8j1Q"; + String key = "6vHEw38JPP6fvNBxVUeb499S7JBfRX5dMGadWrjv3w78UHPD2ac"; + String decryptedKey = BIP38.decrypt("Satoshi", encryptedKey); + assertEquals(decryptedKey, key); + } } \ No newline at end of file