3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-01-31 15:22:16 +00:00

Implement BIP38 password encrypted private keys, decryption only. Contains all the test vectors from the spec minus one that is incompatible to Java.

This commit is contained in:
Andreas Schildbach 2014-07-06 17:25:36 +02:00 committed by Mike Hearn
parent 03652298e1
commit a750a14edd
4 changed files with 373 additions and 12 deletions

View File

@ -89,6 +89,9 @@ import static com.google.common.base.Preconditions.checkState;
public class ECKey implements EncryptableItem, Serializable { public class ECKey implements EncryptableItem, Serializable {
private static final Logger log = LoggerFactory.getLogger(ECKey.class); private static final Logger log = LoggerFactory.getLogger(ECKey.class);
/** The parameters of the secp256k1 curve that Bitcoin uses. */
public static final X9ECParameters CURVE_PARAMS = SECNamedCurves.getByName("secp256k1");
/** The parameters of the secp256k1 curve that Bitcoin uses. */ /** The parameters of the secp256k1 curve that Bitcoin uses. */
public static final ECDomainParameters CURVE; public static final ECDomainParameters CURVE;
@ -103,9 +106,9 @@ public class ECKey implements EncryptableItem, Serializable {
static { static {
// All clients must agree on the curve to use by agreement. Bitcoin uses secp256k1. // All clients must agree on the curve to use by agreement. Bitcoin uses secp256k1.
X9ECParameters params = SECNamedCurves.getByName("secp256k1"); CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(),
CURVE = new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); CURVE_PARAMS.getH());
HALF_CURVE_ORDER = params.getN().shiftRight(1); HALF_CURVE_ORDER = CURVE_PARAMS.getN().shiftRight(1);
secureRandom = new SecureRandom(); secureRandom = new SecureRandom();
} }
@ -178,21 +181,38 @@ public class ECKey implements EncryptableItem, Serializable {
} }
/** /**
* Creates an ECKey given the private key only. The public key is calculated from it (this is slow). Note that * Creates an ECKey given the private key only. The public key is calculated from it (this is slow). The resulting
* the resulting public key is compressed. * public key is compressed.
*/ */
public static ECKey fromPrivate(BigInteger privKey) { public static ECKey fromPrivate(BigInteger privKey) {
return new ECKey(privKey, compressPoint(CURVE.getG().multiply(privKey))); return fromPrivate(privKey, true);
} }
/** /**
* Creates an ECKey given the private key only. The public key is calculated from it (this is slow). The resulting * Creates an ECKey given the private key only. The public key is calculated from it (this is slow), either
* compressed or not.
*/
public static ECKey fromPrivate(BigInteger privKey, boolean compressed) {
ECPoint point = CURVE.getG().multiply(privKey);
return new ECKey(privKey, compressed ? compressPoint(point) : decompressPoint(point));
}
/**
* Creates an ECKey given the private key only. The public key is calculated from it (this is slow). The resulting
* public key is compressed. * public key is compressed.
*/ */
public static ECKey fromPrivate(byte[] privKeyBytes) { public static ECKey fromPrivate(byte[] privKeyBytes) {
return fromPrivate(new BigInteger(1, privKeyBytes)); return fromPrivate(new BigInteger(1, privKeyBytes));
} }
/**
* Creates an ECKey given the private key only. The public key is calculated from it (this is slow), either
* compressed or not.
*/
public static ECKey fromPrivate(byte[] privKeyBytes, boolean compressed) {
return fromPrivate(new BigInteger(1, privKeyBytes), compressed);
}
/** /**
* Creates an ECKey that simply trusts the caller to ensure that point is really the result of multiplying the * Creates an ECKey that simply trusts the caller to ensure that point is really the result of multiplying the
* generator point by the private key. This is used to speed things up when you know you have the right values * generator point by the private key. This is used to speed things up when you know you have the right values
@ -350,7 +370,7 @@ public class ECKey implements EncryptableItem, Serializable {
DERSequenceGenerator seq = new DERSequenceGenerator(baos); DERSequenceGenerator seq = new DERSequenceGenerator(baos);
seq.addObject(new ASN1Integer(1)); // version seq.addObject(new ASN1Integer(1)); // version
seq.addObject(new DEROctetString(privKeyBytes)); seq.addObject(new DEROctetString(privKeyBytes));
seq.addObject(new DERTaggedObject(0, SECNamedCurves.getByName("secp256k1").toASN1Primitive())); seq.addObject(new DERTaggedObject(0, CURVE_PARAMS.toASN1Primitive()));
seq.addObject(new DERTaggedObject(1, new DERBitString(getPubKey()))); seq.addObject(new DERTaggedObject(1, new DERBitString(getPubKey())));
seq.close(); seq.close();
return baos.toByteArray(); return baos.toByteArray();

View File

@ -16,10 +16,14 @@
package com.google.bitcoin.core; package com.google.bitcoin.core;
import static com.google.common.base.Preconditions.checkArgument;
import java.io.Serializable; import java.io.Serializable;
import java.util.Arrays; import java.util.Arrays;
import static com.google.common.base.Preconditions.checkArgument; import org.spongycastle.util.Integers;
import com.google.common.base.Objects;
/** /**
* <p>In Bitcoin the following format is often used to represent some type of key:</p> * <p>In Bitcoin the following format is often used to represent some type of key:</p>
@ -59,10 +63,9 @@ public class VersionedChecksummedBytes implements Serializable {
return Base58.encode(addressBytes); return Base58.encode(addressBytes);
} }
// TODO: shouldn't hashCode be also based on the version?
@Override @Override
public int hashCode() { public int hashCode() {
return Arrays.hashCode(bytes); return Objects.hashCode(version, Arrays.hashCode(bytes));
} }
@Override @Override
@ -70,7 +73,8 @@ public class VersionedChecksummedBytes implements Serializable {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
VersionedChecksummedBytes other = (VersionedChecksummedBytes) o; VersionedChecksummedBytes other = (VersionedChecksummedBytes) o;
return Arrays.equals(bytes, other.bytes); return this.version == other.version
&& Arrays.equals(this.bytes, other.bytes);
} }
/** /**

View File

@ -0,0 +1,180 @@
/*
* Copyright 2014 Andreas Schildbach
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.crypto;
import static com.google.common.base.Preconditions.checkState;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.text.Normalizer;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import com.google.bitcoin.core.AddressFormatException;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.NetworkParameters;
import com.google.bitcoin.core.Sha256Hash;
import com.google.bitcoin.core.VersionedChecksummedBytes;
import com.google.common.base.Charsets;
import com.google.common.base.Objects;
import com.google.common.primitives.Bytes;
import com.lambdaworks.crypto.SCrypt;
/**
* Implementation of <a href="https://github.com/bitcoin/bips/blob/master/bip-0038.mediawiki">BIP 38</a>
* passphrase-protected private keys. Currently, only decryption is supported.
*/
public class BIP38PrivateKey extends VersionedChecksummedBytes {
public final NetworkParameters params;
public final boolean ecMultiply;
public final boolean compressed;
public final boolean hasLotAndSequence;
public final byte[] addressHash;
public final byte[] content;
public static final class BadPassphraseException extends Exception {
}
public BIP38PrivateKey(NetworkParameters params, String encoded) throws AddressFormatException {
super(encoded);
this.params = params;
if (version != 0x01)
throw new AddressFormatException("Mismatched version number: " + version);
if (bytes.length != 38)
throw new AddressFormatException("Wrong number of bytes, excluding version byte: " + bytes.length);
hasLotAndSequence = (bytes[1] & 0x04) != 0; // bit 2
compressed = (bytes[1] & 0x20) != 0; // bit 5
if ((bytes[1] & 0x01) != 0) // bit 0
throw new AddressFormatException("Bit 0x40 reserved for future use.");
if ((bytes[1] & 0x02) != 0) // bit 1
throw new AddressFormatException("Bit 0x80 reserved for future use.");
if ((bytes[1] & 0x08) != 0) // bit 3
throw new AddressFormatException("Bit 0x08 reserved for future use.");
if ((bytes[1] & 0x10) != 0) // bit 4
throw new AddressFormatException("Bit 0x10 reserved for future use.");
final int byte0 = bytes[0] & 0xff;
if (byte0 == 0x42) {
// Non-EC-multiplied key
if ((bytes[1] & 0xc0) != 0xc0) // bits 6+7
throw new AddressFormatException("Bits 0x40 and 0x80 must be set for non-EC-multiplied keys.");
ecMultiply = false;
if (hasLotAndSequence)
throw new AddressFormatException("Non-EC-multiplied keys cannot have lot/sequence.");
} else if (byte0 == 0x43) {
// EC-multiplied key
if ((bytes[1] & 0xc0) != 0x00) // bits 6+7
throw new AddressFormatException("Bits 0x40 and 0x80 must be cleared for EC-multiplied keys.");
ecMultiply = true;
} else {
throw new AddressFormatException("Second byte must by 0x42 or 0x43.");
}
addressHash = Arrays.copyOfRange(bytes, 2, 6);
content = Arrays.copyOfRange(bytes, 6, 38);
}
public ECKey decrypt(String passphrase) throws AddressFormatException, BadPassphraseException {
String normalizedPassphrase = Normalizer.normalize(passphrase, Normalizer.Form.NFC);
ECKey key = ecMultiply ? decryptEC(normalizedPassphrase) : decryptNoEC(normalizedPassphrase);
Sha256Hash hash = Sha256Hash.createDouble(key.toAddress(params).toString().getBytes(Charsets.US_ASCII));
byte[] actualAddressHash = Arrays.copyOfRange(hash.getBytes(), 0, 4);
if (!Arrays.equals(actualAddressHash, addressHash))
throw new BadPassphraseException();
return key;
}
private ECKey decryptNoEC(String normalizedPassphrase) {
try {
byte[] derived = SCrypt.scrypt(normalizedPassphrase.getBytes(Charsets.UTF_8), addressHash, 16384, 8, 8, 64);
byte[] key = Arrays.copyOfRange(derived, 32, 64);
SecretKeySpec keyspec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, keyspec);
byte[] decrypted = cipher.doFinal(content, 0, 32);
for (int i = 0; i < 32; i++)
decrypted[i] ^= derived[i];
return ECKey.fromPrivate(decrypted, compressed);
} catch (GeneralSecurityException x) {
throw new RuntimeException(x);
}
}
private ECKey decryptEC(String normalizedPassphrase) {
try {
byte[] ownerEntropy = Arrays.copyOfRange(content, 0, 8);
byte[] ownerSalt = hasLotAndSequence ? Arrays.copyOfRange(ownerEntropy, 0, 4) : ownerEntropy;
byte[] passFactorBytes = SCrypt.scrypt(normalizedPassphrase.getBytes(Charsets.UTF_8), ownerSalt, 16384, 8, 8, 32);
if (hasLotAndSequence) {
byte[] hashBytes = Bytes.concat(passFactorBytes, ownerEntropy);
checkState(hashBytes.length == 40);
passFactorBytes = Sha256Hash.createDouble(hashBytes).getBytes();
}
BigInteger passFactor = new BigInteger(1, passFactorBytes);
ECKey k = ECKey.fromPrivate(passFactor, true);
byte[] salt = Bytes.concat(addressHash, ownerEntropy);
checkState(salt.length == 12);
byte[] derived = SCrypt.scrypt(k.getPubKey(), salt, 1024, 1, 1, 64);
byte[] aeskey = Arrays.copyOfRange(derived, 32, 64);
SecretKeySpec keyspec = new SecretKeySpec(aeskey, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, keyspec);
byte[] encrypted2 = Arrays.copyOfRange(content, 16, 32);
byte[] decrypted2 = cipher.doFinal(encrypted2);
checkState(decrypted2.length == 16);
for (int i = 0; i < 16; i++)
decrypted2[i] ^= derived[i + 16];
byte[] encrypted1 = Bytes.concat(Arrays.copyOfRange(content, 8, 16), Arrays.copyOfRange(decrypted2, 0, 8));
byte[] decrypted1 = cipher.doFinal(encrypted1);
checkState(decrypted1.length == 16);
for (int i = 0; i < 16; i++)
decrypted1[i] ^= derived[i];
byte[] seed = Bytes.concat(decrypted1, Arrays.copyOfRange(decrypted2, 8, 16));
checkState(seed.length == 24);
BigInteger seedFactor = new BigInteger(1, Sha256Hash.createDouble(seed).getBytes());
checkState(passFactor.signum() >= 0);
checkState(seedFactor.signum() >= 0);
BigInteger priv = passFactor.multiply(seedFactor).mod(ECKey.CURVE_PARAMS.getN());
return ECKey.fromPrivate(priv, compressed);
} catch (GeneralSecurityException x) {
throw new RuntimeException(x);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BIP38PrivateKey other = (BIP38PrivateKey) o;
return super.equals(other)
&& Objects.equal(this.params, other.params);
}
@Override
public int hashCode() {
return Objects.hashCode(super.hashCode(), params);
}
}

View File

@ -0,0 +1,157 @@
/*
* Copyright 2014 Andreas Schildbach
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.crypto;
import static org.junit.Assert.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import org.junit.Ignore;
import org.junit.Test;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.crypto.BIP38PrivateKey.BadPassphraseException;
import com.google.bitcoin.params.MainNetParams;
import com.google.bitcoin.params.TestNet3Params;
public class BIP38PrivateKeyTest {
private static final MainNetParams MAINNET = MainNetParams.get();
private static final TestNet3Params TESTNET = TestNet3Params.get();
@Test
public void bip38testvector_noCompression_noEcMultiply_test1() throws Exception {
BIP38PrivateKey encryptedKey = new BIP38PrivateKey(MAINNET,
"6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg");
ECKey key = encryptedKey.decrypt("TestingOneTwoThree");
assertEquals("5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR", key.getPrivateKeyEncoded(MAINNET)
.toString());
}
@Test
public void bip38testvector_noCompression_noEcMultiply_test2() throws Exception {
BIP38PrivateKey encryptedKey = new BIP38PrivateKey(MAINNET,
"6PRNFFkZc2NZ6dJqFfhRoFNMR9Lnyj7dYGrzdgXXVMXcxoKTePPX1dWByq");
ECKey key = encryptedKey.decrypt("Satoshi");
assertEquals("5HtasZ6ofTHP6HCwTqTkLDuLQisYPah7aUnSKfC7h4hMUVw2gi5", key.getPrivateKeyEncoded(MAINNET)
.toString());
}
@Test
@Ignore("Test disabled because you cannot pass \\u0000 in Strings.")
public void bip38testvector_noCompression_noEcMultiply_test3() throws Exception {
BIP38PrivateKey encryptedKey = new BIP38PrivateKey(MAINNET,
"6PRW5o9FLp4gJDDVqJQKJFTpMvdsSGJxMYHtHaQBF3ooa8mwD69bapcDQn");
ECKey key = encryptedKey.decrypt("\u03d2\u0301\u0000\u00010400\u0001f4a9");
assertEquals("5Jajm8eQ22H3pGWLEVCXyvND8dQZhiQhoLJNKjYXk9roUFTMSZ4", key.getPrivateKeyEncoded(MAINNET)
.toString());
}
@Test
public void bip38testvector_compression_noEcMultiply_test1() throws Exception {
BIP38PrivateKey encryptedKey = new BIP38PrivateKey(MainNetParams.get(),
"6PYNKZ1EAgYgmQfmNVamxyXVWHzK5s6DGhwP4J5o44cvXdoY7sRzhtpUeo");
ECKey key = encryptedKey.decrypt("TestingOneTwoThree");
assertEquals("L44B5gGEpqEDRS9vVPz7QT35jcBG2r3CZwSwQ4fCewXAhAhqGVpP", key.getPrivateKeyEncoded(MAINNET)
.toString());
}
@Test
public void bip38testvector_compression_noEcMultiply_test2() throws Exception {
BIP38PrivateKey encryptedKey = new BIP38PrivateKey(MainNetParams.get(),
"6PYLtMnXvfG3oJde97zRyLYFZCYizPU5T3LwgdYJz1fRhh16bU7u6PPmY7");
ECKey key = encryptedKey.decrypt("Satoshi");
assertEquals("KwYgW8gcxj1JWJXhPSu4Fqwzfhp5Yfi42mdYmMa4XqK7NJxXUSK7", key.getPrivateKeyEncoded(MAINNET)
.toString());
}
@Test
public void bip38testvector_ecMultiply_noCompression_noLotAndSequence_test1() throws Exception {
BIP38PrivateKey encryptedKey = new BIP38PrivateKey(MainNetParams.get(),
"6PfQu77ygVyJLZjfvMLyhLMQbYnu5uguoJJ4kMCLqWwPEdfpwANVS76gTX");
ECKey key = encryptedKey.decrypt("TestingOneTwoThree");
assertEquals("5K4caxezwjGCGfnoPTZ8tMcJBLB7Jvyjv4xxeacadhq8nLisLR2", key.getPrivateKeyEncoded(MAINNET)
.toString());
}
@Test
public void bip38testvector_ecMultiply_noCompression_noLotAndSequence_test2() throws Exception {
BIP38PrivateKey encryptedKey = new BIP38PrivateKey(MainNetParams.get(),
"6PfLGnQs6VZnrNpmVKfjotbnQuaJK4KZoPFrAjx1JMJUa1Ft8gnf5WxfKd");
ECKey key = encryptedKey.decrypt("Satoshi");
assertEquals("5KJ51SgxWaAYR13zd9ReMhJpwrcX47xTJh2D3fGPG9CM8vkv5sH", key.getPrivateKeyEncoded(MAINNET)
.toString());
}
@Test
public void bip38testvector_ecMultiply_noCompression_lotAndSequence_test1() throws Exception {
BIP38PrivateKey encryptedKey = new BIP38PrivateKey(MainNetParams.get(),
"6PgNBNNzDkKdhkT6uJntUXwwzQV8Rr2tZcbkDcuC9DZRsS6AtHts4Ypo1j");
ECKey key = encryptedKey.decrypt("MOLON LABE");
assertEquals("5JLdxTtcTHcfYcmJsNVy1v2PMDx432JPoYcBTVVRHpPaxUrdtf8", key.getPrivateKeyEncoded(MAINNET)
.toString());
}
@Test
public void bip38testvector_ecMultiply_noCompression_lotAndSequence_test2() throws Exception {
BIP38PrivateKey encryptedKey = new BIP38PrivateKey(MainNetParams.get(),
"6PgGWtx25kUg8QWvwuJAgorN6k9FbE25rv5dMRwu5SKMnfpfVe5mar2ngH");
ECKey key = encryptedKey.decrypt("ΜΟΛΩΝ ΛΑΒΕ");
assertEquals("5KMKKuUmAkiNbA3DazMQiLfDq47qs8MAEThm4yL8R2PhV1ov33D", key.getPrivateKeyEncoded(MAINNET)
.toString());
}
@Test
public void bitcoinpaperwallet_testnet() throws Exception {
// values taken from bitcoinpaperwallet.com
BIP38PrivateKey encryptedKey = new BIP38PrivateKey(TESTNET,
"6PRPhQhmtw6dQu6jD8E1KS4VphwJxBS9Eh9C8FQELcrwN3vPvskv9NKvuL");
ECKey key = encryptedKey.decrypt("password");
assertEquals("93MLfjbY6ugAsLeQfFY6zodDa8izgm1XAwA9cpMbUTwLkDitopg", key.getPrivateKeyEncoded(TESTNET)
.toString());
}
@Test
public void bitaddress_testnet() throws Exception {
// values taken from bitaddress.org
BIP38PrivateKey encryptedKey = new BIP38PrivateKey(TESTNET,
"6PfMmVHn153N3x83Yiy4Nf76dHUkXufe2Adr9Fw5bewrunGNeaw2QCpifb");
ECKey key = encryptedKey.decrypt("password");
assertEquals("91tCpdaGr4Khv7UAuUxa6aMqeN5GcPVJxzLtNsnZHTCndxkRcz2", key.getPrivateKeyEncoded(TESTNET)
.toString());
}
@Test(expected = BadPassphraseException.class)
public void badPassphrase() throws Exception {
BIP38PrivateKey encryptedKey = new BIP38PrivateKey(MAINNET,
"6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg");
encryptedKey.decrypt("BAD");
}
@Test
public void testJavaSerialization() throws Exception {
BIP38PrivateKey key = new BIP38PrivateKey(TESTNET, "6PfMmVHn153N3x83Yiy4Nf76dHUkXufe2Adr9Fw5bewrunGNeaw2QCpifb");
ByteArrayOutputStream os = new ByteArrayOutputStream();
new ObjectOutputStream(os).writeObject(key);
BIP38PrivateKey keyCopy = (BIP38PrivateKey) new ObjectInputStream(new ByteArrayInputStream(os.toByteArray()))
.readObject();
assertEquals(key, keyCopy);
}
}