From a9d3a845e6aebff6fdfd12325e4c1ace84ca9e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dirk=20Ja=CC=88ckel?= Date: Wed, 3 Dec 2014 23:02:53 +0100 Subject: [PATCH 1/2] EtherSaleWallet --- ethereumj-core/pom.xml | 5 + .../org/ethereum/wallet/EtherSaleWallet.java | 61 ++++++++++++ .../wallet/EtherSaleWalletDecoder.java | 84 ++++++++++++++++ .../wallet/EtherSaleWalletDecoderTest.java | 96 +++++++++++++++++++ .../resources/wallet/ethersalewallet.json | 6 ++ .../wallet/ethersalewallet_broken.json | 6 ++ 6 files changed, 258 insertions(+) create mode 100644 ethereumj-core/src/main/java/org/ethereum/wallet/EtherSaleWallet.java create mode 100644 ethereumj-core/src/main/java/org/ethereum/wallet/EtherSaleWalletDecoder.java create mode 100644 ethereumj-core/src/test/java/org/ethereum/wallet/EtherSaleWalletDecoderTest.java create mode 100644 ethereumj-core/src/test/resources/wallet/ethersalewallet.json create mode 100644 ethereumj-core/src/test/resources/wallet/ethersalewallet_broken.json diff --git a/ethereumj-core/pom.xml b/ethereumj-core/pom.xml index 8df91f37..0028f66f 100644 --- a/ethereumj-core/pom.xml +++ b/ethereumj-core/pom.xml @@ -63,6 +63,11 @@ core ${spongycastle.version} + + com.madgag.spongycastle + prov + ${spongycastle.version} + org.iq80.leveldb leveldb diff --git a/ethereumj-core/src/main/java/org/ethereum/wallet/EtherSaleWallet.java b/ethereumj-core/src/main/java/org/ethereum/wallet/EtherSaleWallet.java new file mode 100644 index 00000000..059f63a1 --- /dev/null +++ b/ethereumj-core/src/main/java/org/ethereum/wallet/EtherSaleWallet.java @@ -0,0 +1,61 @@ +package org.ethereum.wallet; + +import javax.xml.bind.DatatypeConverter; + +public class EtherSaleWallet { + + private String encseed; + private String ethaddr; + private String email; + private String btcaddr; + + public String getEncseed() { + return encseed; + } + + public byte[] getEncseedBytes() { + return DatatypeConverter.parseHexBinary(encseed); + } + + public void setEncseed(String encseed) { + this.encseed = encseed; + } + + public String getEthaddr() { + return ethaddr; + } + + public byte[] getEthaddrBytes() { + return DatatypeConverter.parseHexBinary(ethaddr); + } + + public void setEthaddr(String ethaddr) { + this.ethaddr = ethaddr; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getBtcaddr() { + return btcaddr; + } + + public void setBtcaddr(String btcaddr) { + this.btcaddr = btcaddr; + } + + @Override + public String toString() { + return "EtherSaleWallet{" + + "encseed='" + encseed + '\'' + + ", ethaddr='" + ethaddr + '\'' + + ", email='" + email + '\'' + + ", btcaddr='" + btcaddr + '\'' + + '}'; + } +} diff --git a/ethereumj-core/src/main/java/org/ethereum/wallet/EtherSaleWalletDecoder.java b/ethereumj-core/src/main/java/org/ethereum/wallet/EtherSaleWalletDecoder.java new file mode 100644 index 00000000..1b550c58 --- /dev/null +++ b/ethereumj-core/src/main/java/org/ethereum/wallet/EtherSaleWalletDecoder.java @@ -0,0 +1,84 @@ +package org.ethereum.wallet; + +import org.spongycastle.crypto.BufferedBlockCipher; +import org.spongycastle.crypto.CipherParameters; +import org.spongycastle.crypto.InvalidCipherTextException; +import org.spongycastle.crypto.PBEParametersGenerator; +import org.spongycastle.crypto.digests.SHA256Digest; +import org.spongycastle.crypto.engines.AESEngine; +import org.spongycastle.crypto.generators.PKCS5S2ParametersGenerator; +import org.spongycastle.crypto.modes.CBCBlockCipher; +import org.spongycastle.crypto.paddings.BlockCipherPadding; +import org.spongycastle.crypto.paddings.PKCS7Padding; +import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.spongycastle.crypto.params.KeyParameter; +import org.spongycastle.crypto.params.ParametersWithIV; +import org.spongycastle.jcajce.provider.digest.SHA3; + +import java.security.MessageDigest; +import java.util.Arrays; + + +public class EtherSaleWalletDecoder { + + public static final int HASH_ITERATIONS = 2000; + public static final int DERIVED_KEY_BIT_COUNT = 128; + public static final int IV_LENGTH = 16; + + private EtherSaleWallet etherSaleWallet; + + public EtherSaleWalletDecoder(final EtherSaleWallet wallet) { + etherSaleWallet = wallet; + } + + public byte[] getPrivateKey(final String password) throws InvalidCipherTextException { + byte[] passwordHash = generatePasswordHash(password); + byte[] decryptedSeed = decryptSeed(passwordHash, etherSaleWallet.getEncseedBytes()); + return hashSeed(decryptedSeed); + } + + /* VisibleForTesting */ + protected byte[] generatePasswordHash(final String password) { + char[] chars = password.toCharArray(); + byte[] salt = password.getBytes(); + + PKCS5S2ParametersGenerator generator = new PKCS5S2ParametersGenerator(new SHA256Digest()); + generator.init(PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(chars), salt, HASH_ITERATIONS); + return ((KeyParameter) generator.generateDerivedParameters(DERIVED_KEY_BIT_COUNT)).getKey(); + } + + private byte[] hashSeed(final byte[] seed) { + MessageDigest md = new SHA3.Digest256(); + return md.digest(seed); + } + + protected byte[] decryptSeed(byte[] pbkdf2PasswordHash, byte[] encseedBytesWithIV) throws InvalidCipherTextException { + + // first 16 bytes are the IV (0-15) + byte[] ivBytes = Arrays.copyOf(encseedBytesWithIV, IV_LENGTH); + // use bytes 16 to the end for encrypted seed + byte[] encData = Arrays.copyOfRange(encseedBytesWithIV, IV_LENGTH, encseedBytesWithIV.length); + + // setup cipher parameters with key and IV + KeyParameter keyParam = new KeyParameter(pbkdf2PasswordHash); + CipherParameters params = new ParametersWithIV(keyParam, ivBytes); + + // setup AES cipher in CBC mode with PKCS7 padding + BlockCipherPadding padding = new PKCS7Padding(); + BufferedBlockCipher cipher = new PaddedBufferedBlockCipher( + new CBCBlockCipher(new AESEngine()), padding); + cipher.reset(); + cipher.init(false, params); + + // create a temporary buffer to decode into (it'll include padding) + byte[] buffer = new byte[cipher.getOutputSize(encData.length)]; + int length = cipher.processBytes(encData, 0, encData.length, buffer, 0); + length += cipher.doFinal(buffer, length); + + // remove padding + byte[] result = new byte[length]; + System.arraycopy(buffer, 0, result, 0, length); + + return result; + } +} diff --git a/ethereumj-core/src/test/java/org/ethereum/wallet/EtherSaleWalletDecoderTest.java b/ethereumj-core/src/test/java/org/ethereum/wallet/EtherSaleWalletDecoderTest.java new file mode 100644 index 00000000..16866061 --- /dev/null +++ b/ethereumj-core/src/test/java/org/ethereum/wallet/EtherSaleWalletDecoderTest.java @@ -0,0 +1,96 @@ +package org.ethereum.wallet; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.ethereum.crypto.ECKey; +import org.junit.Before; +import org.junit.Test; +import org.spongycastle.crypto.InvalidCipherTextException; +import org.spongycastle.util.encoders.Hex; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +public class EtherSaleWalletDecoderTest { + + private ObjectMapper mapper = new ObjectMapper(); + private EtherSaleWalletDecoder walletDecoder; + private EtherSaleWallet etherSaleWallet; + + @Before + public void setUp() throws IOException { + etherSaleWallet = mapper.readValue(getClass().getResourceAsStream("/wallet/ethersalewallet.json"), EtherSaleWallet.class); + walletDecoder = new EtherSaleWalletDecoder(etherSaleWallet); + } + + @Test + public void shouldGeneratePasswordHash() throws InvalidKeySpecException, NoSuchAlgorithmException { + + byte[] result = walletDecoder.generatePasswordHash("foobar"); + String resultString = Hex.toHexString(result); + + assertThat(resultString.toLowerCase(), is("d11d0652ee9ca94500ba8301903c8f6a")); + } + + @Test + public void shouldGeneratePasswordHashWithUmlauts() throws InvalidKeySpecException, NoSuchAlgorithmException { + + byte[] result = walletDecoder.generatePasswordHash("öäüß"); + String resultString = Hex.toHexString(result); + + assertThat(resultString.toLowerCase(), is("67162c127acd9ac55a75a8c5367b9a1a")); + } + + @Test + public void shouldGeneratePasswordHashWithUnicode() throws InvalidKeySpecException, NoSuchAlgorithmException { + + byte[] result = walletDecoder.generatePasswordHash("☯"); + String resultString = Hex.toHexString(result); + + assertThat(resultString.toLowerCase(), is("47204606123eae746a633c632904d94f")); + } + + @Test + public void shouldDecryptSeed() throws InvalidCipherTextException { + byte[] result = walletDecoder.decryptSeed(walletDecoder.generatePasswordHash("foobar"), etherSaleWallet.getEncseedBytes()); + String resultString = Hex.toHexString(result); + + assertThat(resultString.toLowerCase(), is("37343165366130323566656533363039626262613564366430373038353964643534623862646231653232333431363133653462623832643333313537663035")); + } + + @Test + public void shouldGetPrivateKey() throws InvalidCipherTextException { + byte[] result = walletDecoder.getPrivateKey("foobar"); + String resultString = Hex.toHexString(result); + + assertThat(resultString.toLowerCase(), is("74ef8a796480dda87b4bc550b94c408ad386af0f65926a392136286784d63858")); + } + + @Test(expected = InvalidCipherTextException.class) + public void shouldRejectWrongPassword() throws InvalidCipherTextException { + walletDecoder.getPrivateKey("foo"); + } + + @Test(expected = InvalidCipherTextException.class) + public void shouldRejectWrongPasswordSameLength() throws InvalidCipherTextException { + walletDecoder.getPrivateKey("barfoo"); + } + + @Test + public void ethereumAddressShouldMatchPrivateKey() throws InvalidCipherTextException { + BigInteger privKey = new BigInteger(walletDecoder.getPrivateKey("foobar")); + byte[] addr = ECKey.fromPrivate(privKey).getAddress(); + assertThat(Hex.toHexString(etherSaleWallet.getEthaddrBytes()), is(Hex.toHexString(addr))); + } + + @Test(expected = InvalidCipherTextException.class) + public void shouldHandleBrokenWallet() throws IOException, InvalidCipherTextException { + EtherSaleWallet brokenEtherSaleWallet = mapper.readValue(getClass().getResourceAsStream("/wallet/ethersalewallet_broken.json"), EtherSaleWallet.class); + EtherSaleWalletDecoder walletDecoder = new EtherSaleWalletDecoder(brokenEtherSaleWallet); + walletDecoder.getPrivateKey("foobar"); + } +} diff --git a/ethereumj-core/src/test/resources/wallet/ethersalewallet.json b/ethereumj-core/src/test/resources/wallet/ethersalewallet.json new file mode 100644 index 00000000..3e037817 --- /dev/null +++ b/ethereumj-core/src/test/resources/wallet/ethersalewallet.json @@ -0,0 +1,6 @@ +{ + "encseed" : "957e46d54c10da45351554eb60d731b164043743dfb212ccf1827491a01cf344b390e4ac5af640e4f54ff28b046e48dc84094b99011d72ca79f2da9aa2792f4d2b8545455ca8dbba15d69048b3f95eccfed2d19427abcb2c9483e7491163eb1b", + "ethaddr" : "ba73facb4f8291f09f27f90fe1213537b910065e", + "email" : "foo@bar.com", + "btcaddr" : "1JEQr5LHrY8yVmWFaB31BVa9Y6sLQ9Kg41" +} \ No newline at end of file diff --git a/ethereumj-core/src/test/resources/wallet/ethersalewallet_broken.json b/ethereumj-core/src/test/resources/wallet/ethersalewallet_broken.json new file mode 100644 index 00000000..3c7775a1 --- /dev/null +++ b/ethereumj-core/src/test/resources/wallet/ethersalewallet_broken.json @@ -0,0 +1,6 @@ +{ + "encseed" : "957e46d54c10da45351554eb60d731b164043743dfb212ccf1827491a01cf344b390e4ac5af640e4f54ff28b046e48dc84094b99011d72ca79f2da9aa2792f4d2b8545455ca8dbba15d69048b3f95eccfed2d19427abcb2c9483e7491163eb1c", + "ethaddr" : "ba73facb4f8291f09f27f90fe1213537b910065e", + "email" : "foo@bar.com", + "btcaddr" : "1JEQr5LHrY8yVmWFaB31BVa9Y6sLQ9Kg41" +} \ No newline at end of file From c1bae97740602f746832d3cc13eca214a6686ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dirk=20Ja=CC=88ckel?= Date: Thu, 4 Dec 2014 01:03:05 +0100 Subject: [PATCH 2/2] Avoid JCE provider for SHA3 --- ethereumj-core/pom.xml | 5 ----- .../wallet/EtherSaleWalletDecoder.java | 18 ++++++++---------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/ethereumj-core/pom.xml b/ethereumj-core/pom.xml index 0028f66f..8df91f37 100644 --- a/ethereumj-core/pom.xml +++ b/ethereumj-core/pom.xml @@ -63,11 +63,6 @@ core ${spongycastle.version} - - com.madgag.spongycastle - prov - ${spongycastle.version} - org.iq80.leveldb leveldb diff --git a/ethereumj-core/src/main/java/org/ethereum/wallet/EtherSaleWalletDecoder.java b/ethereumj-core/src/main/java/org/ethereum/wallet/EtherSaleWalletDecoder.java index 1b550c58..ec1956ee 100644 --- a/ethereumj-core/src/main/java/org/ethereum/wallet/EtherSaleWalletDecoder.java +++ b/ethereumj-core/src/main/java/org/ethereum/wallet/EtherSaleWalletDecoder.java @@ -1,10 +1,8 @@ package org.ethereum.wallet; -import org.spongycastle.crypto.BufferedBlockCipher; -import org.spongycastle.crypto.CipherParameters; -import org.spongycastle.crypto.InvalidCipherTextException; -import org.spongycastle.crypto.PBEParametersGenerator; +import org.spongycastle.crypto.*; import org.spongycastle.crypto.digests.SHA256Digest; +import org.spongycastle.crypto.digests.SHA3Digest; import org.spongycastle.crypto.engines.AESEngine; import org.spongycastle.crypto.generators.PKCS5S2ParametersGenerator; import org.spongycastle.crypto.modes.CBCBlockCipher; @@ -13,9 +11,7 @@ import org.spongycastle.crypto.paddings.PKCS7Padding; import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; import org.spongycastle.crypto.params.KeyParameter; import org.spongycastle.crypto.params.ParametersWithIV; -import org.spongycastle.jcajce.provider.digest.SHA3; -import java.security.MessageDigest; import java.util.Arrays; @@ -48,8 +44,11 @@ public class EtherSaleWalletDecoder { } private byte[] hashSeed(final byte[] seed) { - MessageDigest md = new SHA3.Digest256(); - return md.digest(seed); + ExtendedDigest md = new SHA3Digest(256); + md.update(seed, 0, seed.length); + byte[] result = new byte[md.getDigestSize()]; + md.doFinal(result, 0); + return result; } protected byte[] decryptSeed(byte[] pbkdf2PasswordHash, byte[] encseedBytesWithIV) throws InvalidCipherTextException { @@ -65,8 +64,7 @@ public class EtherSaleWalletDecoder { // setup AES cipher in CBC mode with PKCS7 padding BlockCipherPadding padding = new PKCS7Padding(); - BufferedBlockCipher cipher = new PaddedBufferedBlockCipher( - new CBCBlockCipher(new AESEngine()), padding); + BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()), padding); cipher.reset(); cipher.init(false, params);