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