mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-26 01:56:44 +00:00
add bip38 decryption functionality and tests
This commit is contained in:
parent
5de3abd362
commit
f10688279a
3 changed files with 205 additions and 0 deletions
|
@ -121,6 +121,13 @@ public class Utils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static byte[] concat(byte[] a, byte[] b) {
|
||||||
|
byte[] c = new byte[a.length + b.length];
|
||||||
|
System.arraycopy(a, 0, c, 0, a.length);
|
||||||
|
System.arraycopy(b, 0, c, a.length, b.length);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
/** Parse 4 bytes from the byte array (starting at the offset) as unsigned 32-bit integer in little endian format. */
|
/** Parse 4 bytes from the byte array (starting at the offset) as unsigned 32-bit integer in little endian format. */
|
||||||
public static long readUint32(byte[] bytes, int offset) {
|
public static long readUint32(byte[] bytes, int offset) {
|
||||||
return (bytes[offset] & 0xffl) |
|
return (bytes[offset] & 0xffl) |
|
||||||
|
|
165
src/main/java/com/sparrowwallet/drongo/crypto/BIP38.java
Normal file
165
src/main/java/com/sparrowwallet/drongo/crypto/BIP38.java
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
/**
|
||||||
|
* Implementation of BIP38 encryption / decryption / key-address generation
|
||||||
|
* Based on https://github.com/bitcoin/bips/blob/master/bip-0038.mediawiki
|
||||||
|
*
|
||||||
|
* Tips much appreciated: 1EmwBbfgH7BPMoCpcFzyzgAN9Ya7jm8L1Z :)
|
||||||
|
*
|
||||||
|
* Copyright 2014 Diego Basch
|
||||||
|
*
|
||||||
|
* 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.sparrowwallet.drongo.crypto;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.Drongo;
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Base58;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
|
import org.bouncycastle.crypto.generators.SCrypt;
|
||||||
|
import org.bouncycastle.math.ec.ECPoint;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.drongo.crypto.ECKey.CURVE;
|
||||||
|
|
||||||
|
public class BIP38 {
|
||||||
|
/**
|
||||||
|
* Decrypts an encrypted key.
|
||||||
|
* @param passphrase
|
||||||
|
* @param encryptedKey
|
||||||
|
* @throws UnsupportedEncodingException
|
||||||
|
* @throws GeneralSecurityException
|
||||||
|
*/
|
||||||
|
public static DumpedPrivateKey decrypt(String passphrase, String encryptedKey) throws UnsupportedEncodingException, GeneralSecurityException {
|
||||||
|
byte[] encryptedKeyBytes = Base58.decodeChecked(encryptedKey);
|
||||||
|
DumpedPrivateKey result;
|
||||||
|
byte ec = encryptedKeyBytes[1];
|
||||||
|
switch (ec) {
|
||||||
|
case 0x43: result = decryptEC(passphrase, encryptedKeyBytes);
|
||||||
|
break;
|
||||||
|
case 0x42: result = decryptNoEC(passphrase, encryptedKeyBytes);
|
||||||
|
break;
|
||||||
|
default: throw new RuntimeException("Invalid key - second byte is: " + ec);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts a key encrypted with EC multiplication
|
||||||
|
* @param passphrase
|
||||||
|
* @param encryptedKey
|
||||||
|
* @throws UnsupportedEncodingException
|
||||||
|
* @throws GeneralSecurityException
|
||||||
|
*/
|
||||||
|
public static DumpedPrivateKey decryptEC(String passphrase, byte[] encryptedKey) throws UnsupportedEncodingException, GeneralSecurityException {
|
||||||
|
|
||||||
|
byte flagByte = encryptedKey[2];
|
||||||
|
byte[] passFactor;
|
||||||
|
boolean hasLot = (flagByte & 4) == 4;
|
||||||
|
byte[] ownerSalt = Arrays.copyOfRange(encryptedKey, 7, 15 - (flagByte & 4));
|
||||||
|
if (!hasLot) {
|
||||||
|
passFactor = SCrypt.generate(passphrase.getBytes("UTF8"), ownerSalt, 16384, 8, 8, 32);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
byte[] preFactor = SCrypt.generate(passphrase.getBytes("UTF8"), ownerSalt, 16384, 8, 8, 32);
|
||||||
|
byte[] ownerEntropy = Arrays.copyOfRange(encryptedKey, 7, 15);
|
||||||
|
byte[] tmp = Utils.concat(preFactor, ownerEntropy);
|
||||||
|
passFactor = Sha256Hash.hashTwice(tmp, 0, 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] addressHash = Arrays.copyOfRange(encryptedKey, 3, 7);
|
||||||
|
ECPoint g = CURVE.getG();
|
||||||
|
ECPoint p = g.multiply(new BigInteger(1, passFactor));
|
||||||
|
byte[] passPoint = p.getEncoded(true);
|
||||||
|
byte[] salt = new byte[12];
|
||||||
|
byte[] encryptedPart2 = Arrays.copyOfRange(encryptedKey, 23, 39);
|
||||||
|
System.arraycopy(addressHash, 0, salt, 0, 4);
|
||||||
|
System.arraycopy(encryptedKey, 7, salt, 4, 8);
|
||||||
|
|
||||||
|
byte[] secondKey = SCrypt.generate(passPoint, salt, 1024, 1, 1, 64);
|
||||||
|
byte[] derivedHalf1 = Arrays.copyOfRange(secondKey, 0, 32);
|
||||||
|
byte[] derivedHalf2 = Arrays.copyOfRange(secondKey, 32, 64);
|
||||||
|
byte[] m2 = decryptAES(encryptedPart2, derivedHalf2);
|
||||||
|
|
||||||
|
byte[] encryptedPart1 = new byte[16];
|
||||||
|
System.arraycopy(encryptedKey, 15, encryptedPart1, 0, 8);
|
||||||
|
|
||||||
|
byte[] seedB = new byte[24];
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
m2[i] = (byte) (m2[i] ^ derivedHalf1[16 + i]);
|
||||||
|
}
|
||||||
|
System.arraycopy(m2, 0, encryptedPart1, 8, 8);
|
||||||
|
|
||||||
|
byte[] m1 = decryptAES(encryptedPart1, derivedHalf2);
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
seedB[i] = (byte) (m1[i] ^ derivedHalf1[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.arraycopy(m2, 8, seedB, 16, 8);
|
||||||
|
byte[] factorB = Sha256Hash.hashTwice(seedB, 0, 24);
|
||||||
|
BigInteger n = CURVE.getN();
|
||||||
|
BigInteger pk = new BigInteger(1, passFactor).multiply(new BigInteger(1, factorB)).remainder(n);
|
||||||
|
|
||||||
|
ECKey privKey = ECKey.fromPrivate(pk, false);
|
||||||
|
return privKey.getPrivateKeyEncoded();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts a key that was encrypted without EC multiplication.
|
||||||
|
* @param passphrase
|
||||||
|
* @param encryptedKey
|
||||||
|
* @throws UnsupportedEncodingException
|
||||||
|
* @throws GeneralSecurityException
|
||||||
|
*/
|
||||||
|
public static DumpedPrivateKey decryptNoEC(String passphrase, byte[] encryptedKey) throws UnsupportedEncodingException, GeneralSecurityException {
|
||||||
|
|
||||||
|
byte[] addressHash = Arrays.copyOfRange(encryptedKey, 3, 7);
|
||||||
|
byte[] scryptKey = SCrypt.generate(passphrase.getBytes("UTF8"), addressHash, 16384, 8, 8, 64);
|
||||||
|
byte[] derivedHalf1 = Arrays.copyOfRange(scryptKey, 0, 32);
|
||||||
|
byte[] derivedHalf2 = Arrays.copyOfRange(scryptKey, 32, 64);
|
||||||
|
|
||||||
|
byte[] encryptedHalf1 = Arrays.copyOfRange(encryptedKey, 7, 23);
|
||||||
|
byte[] encryptedHalf2 = Arrays.copyOfRange(encryptedKey, 23, 39);
|
||||||
|
byte[] k1 = decryptAES(encryptedHalf1, derivedHalf2);
|
||||||
|
byte[] k2 = decryptAES(encryptedHalf2, derivedHalf2);
|
||||||
|
byte[] keyBytes = new byte[32];
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
keyBytes[i] = (byte) (k1[i] ^ derivedHalf1[i]);
|
||||||
|
keyBytes[i + 16] = (byte) (k2[i] ^ derivedHalf1[i + 16]);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean compressed = (encryptedKey[2] & (byte) 0x20) == 0x20;
|
||||||
|
ECKey k = new ECKey(new BigInteger(1, keyBytes), null, compressed);
|
||||||
|
return k.getPrivateKeyEncoded();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts ciphertext with AES
|
||||||
|
* @param ciphertext
|
||||||
|
* @param key
|
||||||
|
* @throws GeneralSecurityException
|
||||||
|
*/
|
||||||
|
public static byte[] decryptAES(byte[] ciphertext, byte[] key) throws GeneralSecurityException {
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding", Drongo.getProvider());
|
||||||
|
SecretKeySpec aesKey = new SecretKeySpec(key, "AES");
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, aesKey);
|
||||||
|
return cipher.doFinal(ciphertext);
|
||||||
|
}
|
||||||
|
}
|
33
src/test/java/com/sparrowwallet/drongo/crypto/BIP38Test.java
Normal file
33
src/test/java/com/sparrowwallet/drongo/crypto/BIP38Test.java
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package com.sparrowwallet.drongo.crypto;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
|
||||||
|
public class BIP38Test {
|
||||||
|
@Test
|
||||||
|
public void testNoCompressionNoEC() throws GeneralSecurityException, UnsupportedEncodingException {
|
||||||
|
Assert.assertEquals("5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR", BIP38.decrypt("TestingOneTwoThree", "6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg").toString()); ;
|
||||||
|
Assert.assertEquals("5HtasZ6ofTHP6HCwTqTkLDuLQisYPah7aUnSKfC7h4hMUVw2gi5", BIP38.decrypt("Satoshi", "6PRNFFkZc2NZ6dJqFfhRoFNMR9Lnyj7dYGrzdgXXVMXcxoKTePPX1dWByq").toString()); ;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCompressionNoEC() throws GeneralSecurityException, UnsupportedEncodingException {
|
||||||
|
Assert.assertEquals("L44B5gGEpqEDRS9vVPz7QT35jcBG2r3CZwSwQ4fCewXAhAhqGVpP", BIP38.decrypt("TestingOneTwoThree", "6PYNKZ1EAgYgmQfmNVamxyXVWHzK5s6DGhwP4J5o44cvXdoY7sRzhtpUeo").toString()); ;
|
||||||
|
Assert.assertEquals("KwYgW8gcxj1JWJXhPSu4Fqwzfhp5Yfi42mdYmMa4XqK7NJxXUSK7", BIP38.decrypt("Satoshi", "6PYLtMnXvfG3oJde97zRyLYFZCYizPU5T3LwgdYJz1fRhh16bU7u6PPmY7").toString()); ;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCompressionEC() throws GeneralSecurityException, UnsupportedEncodingException {
|
||||||
|
Assert.assertEquals("5K4caxezwjGCGfnoPTZ8tMcJBLB7Jvyjv4xxeacadhq8nLisLR2", BIP38.decrypt("TestingOneTwoThree", "6PfQu77ygVyJLZjfvMLyhLMQbYnu5uguoJJ4kMCLqWwPEdfpwANVS76gTX").toString()); ;
|
||||||
|
Assert.assertEquals("5KJ51SgxWaAYR13zd9ReMhJpwrcX47xTJh2D3fGPG9CM8vkv5sH", BIP38.decrypt("Satoshi", "6PfLGnQs6VZnrNpmVKfjotbnQuaJK4KZoPFrAjx1JMJUa1Ft8gnf5WxfKd").toString()); ;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCompressionECLot() throws GeneralSecurityException, UnsupportedEncodingException {
|
||||||
|
Assert.assertEquals("5JLdxTtcTHcfYcmJsNVy1v2PMDx432JPoYcBTVVRHpPaxUrdtf8", BIP38.decrypt("MOLON LABE", "6PgNBNNzDkKdhkT6uJntUXwwzQV8Rr2tZcbkDcuC9DZRsS6AtHts4Ypo1j").toString()); ;
|
||||||
|
Assert.assertEquals("5KMKKuUmAkiNbA3DazMQiLfDq47qs8MAEThm4yL8R2PhV1ov33D", BIP38.decrypt("ΜΟΛΩΝ ΛΑΒΕ", "6PgGWtx25kUg8QWvwuJAgorN6k9FbE25rv5dMRwu5SKMnfpfVe5mar2ngH").toString()); ;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue