commit
c9ea4c07d4
|
@ -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 + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package org.ethereum.wallet;
|
||||||
|
|
||||||
|
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;
|
||||||
|
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 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) {
|
||||||
|
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 {
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"encseed" : "957e46d54c10da45351554eb60d731b164043743dfb212ccf1827491a01cf344b390e4ac5af640e4f54ff28b046e48dc84094b99011d72ca79f2da9aa2792f4d2b8545455ca8dbba15d69048b3f95eccfed2d19427abcb2c9483e7491163eb1b",
|
||||||
|
"ethaddr" : "ba73facb4f8291f09f27f90fe1213537b910065e",
|
||||||
|
"email" : "foo@bar.com",
|
||||||
|
"btcaddr" : "1JEQr5LHrY8yVmWFaB31BVa9Y6sLQ9Kg41"
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"encseed" : "957e46d54c10da45351554eb60d731b164043743dfb212ccf1827491a01cf344b390e4ac5af640e4f54ff28b046e48dc84094b99011d72ca79f2da9aa2792f4d2b8545455ca8dbba15d69048b3f95eccfed2d19427abcb2c9483e7491163eb1c",
|
||||||
|
"ethaddr" : "ba73facb4f8291f09f27f90fe1213537b910065e",
|
||||||
|
"email" : "foo@bar.com",
|
||||||
|
"btcaddr" : "1JEQr5LHrY8yVmWFaB31BVa9Y6sLQ9Kg41"
|
||||||
|
}
|
Loading…
Reference in New Issue