BIP38 is a ridiculously tough encryption, it is also memory intensive!
I tried bruteforcing one a few days back with a C# implementation and it takes 0.25 seconds to decrypt one on a fourth generation quad core i5 processor. Memory usage = 300MB. Despite my best effort to optimize the code… (100% CPU utilization).
Having a password of at least 6 character will ensure that it’ll take 8000 years on a single machine. (72 ^ 6 tries)
Great! I support that since its safe.
public Exception DecryptWithPassphrase(string passphrase) {
// check for null entry
if (passphrase == null || passphrase == "") {
return new ArgumentException("Passphrase is required");
}
Bip38Intermediate intermediate = new Bip38Intermediate(passphrase, _ownerentropy, LotSequencePresent);
// derive the 64 bytes we need
// get ECPoint from passpoint
PublicKey pk = new PublicKey(intermediate.passpoint);
byte[] addresshashplusownerentropy = Util.ConcatenateByteArrays(_addressHash, intermediate.ownerentropy);
// derive encryption key material
byte[] derived = new byte[64];
SCrypt.ComputeKey(intermediate.passpoint, addresshashplusownerentropy, 1024, 1, 1, 1, derived);
byte[] derivedhalf2 = new byte[32];
Array.Copy(derived, 32, derivedhalf2, 0, 32);
byte[] unencryptedpubkey = new byte[33];
// recover the 0x02 or 0x03 prefix
unencryptedpubkey[0] = (byte)(_encryptedpointb[0] ^ (derived[63] & 0x01));
// decrypt
var aes = Aes.Create();
aes.KeySize = 256;
aes.Mode = CipherMode.ECB;
aes.Key = derivedhalf2;
ICryptoTransform decryptor = aes.CreateDecryptor();
decryptor.TransformBlock(_encryptedpointb, 1, 16, unencryptedpubkey, 1);
decryptor.TransformBlock(_encryptedpointb, 1, 16, unencryptedpubkey, 1);
decryptor.TransformBlock(_encryptedpointb, 1 + 16, 16, unencryptedpubkey, 17);
decryptor.TransformBlock(_encryptedpointb, 1 + 16, 16, unencryptedpubkey, 17);
// xor out the padding
for (int i = 0; i < 32; i++) unencryptedpubkey[i + 1] ^= derived[i];
// reconstitute the ECPoint
var ps = Org.BouncyCastle.Asn1.Sec.SecNamedCurves.GetByName("secp256k1");
ECPoint point;
try {
point = ps.Curve.DecodePoint(unencryptedpubkey);
// multiply passfactor. Result is going to be compressed.
ECPoint pubpoint = point.Multiply(new BigInteger(1, intermediate.passfactor));
// Do we want it uncompressed? then we will have to uncompress it.
if (IsCompressedPoint==false) {
pubpoint = ps.Curve.CreatePoint(pubpoint.X.ToBigInteger(), pubpoint.Y.ToBigInteger(), false);
}
// Convert to bitcoin address and check address hash.
PublicKey generatedaddress = new PublicKey(pubpoint);
// get addresshash
UTF8Encoding utf8 = new UTF8Encoding(false);
Sha256Digest sha256 = new Sha256Digest();
byte[] generatedaddressbytes = utf8.GetBytes(generatedaddress.AddressBase58);
sha256.BlockUpdate(generatedaddressbytes, 0, generatedaddressbytes.Length);
byte[] addresshashfull = new byte[32];
sha256.DoFinal(addresshashfull, 0);
sha256.BlockUpdate(addresshashfull, 0, 32);
sha256.DoFinal(addresshashfull, 0);
for (int i = 0; i < 4; i++) {
if (addresshashfull[i] != _addressHash[i]) {
return new ArgumentException("This passphrase is wrong or does not belong to this confirmation code.");
}
}
this.PublicKey = generatedaddress;
} catch {
return new ArgumentException("This passphrase is wrong or does not belong to this confirmation code.");
}
return null;
}
}
#region Bruteforce
/* An array containing the characters which will be used to create the brute force keys,
* if less characters are used (e.g. only lower case chars) the faster the password is matched */
private static char[] charactersToTest =
{
'1','2','3','4','5','6','7','8','9','0',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z','A','B','C','D','E',
'F','G','H','I','J','K','L','M','N','O','P','Q','R',
'S','T','U','V','W','X','Y','Z','!','$','#','@','-'
};
/* The length of the charactersToTest Array is stored in a
* additional variable to increase performance */
private static int charactersToTestLength = charactersToTest.Length;
private static long computedKeys = 0;
private bool passFound = false;
private string pass = null;
private string currentComputingPass = "";
private System.Windows.Forms.Timer updateTimer;
private void button_bruteforcePrivKey_Click(object sender, EventArgs e)
{
// Disable button
button_bruteforcePrivKey.Enabled = false;
// Referencing
string privateKey = txtPrivWIF.Text; // 6PfR1Z9cEHVsHaQTzggoaZmEeQVQAqPbR6A5gd3Eop3JBTZ9oQLUi7hShs
passFound = false;
pass = null;
currentComputingPass = "";
int threads = 10;
int.TryParse(textBox_threads.Text, out threads);
if (updateTimer != null)
{
updateTimer.Stop();
updateTimer = null;
}
updateTimer = new System.Windows.Forms.Timer();
updateTimer.Interval = 100;
updateTimer.Tick += updateTimer_Tick;
updateTimer.Start();
try
{
object interpretation = StringInterpreter.Interpret(privateKey, compressed: compressToolStripMenuItem.Checked, addressType: this.AddressTypeByte);
if (interpretation is PassphraseKeyPair)
{
string passPhrase = txtPassphrase.Text;
PassphraseKeyPair ppkp = (PassphraseKeyPair)interpretation;
Task.Run(() =>
{
startBruteForce(6, ppkp);
});
}
}
catch (Exception ae)
{
MessageBox.Show(ae.Message);
}
finally
{
}
}
void updateTimer_Tick(object sender, EventArgs e)
{
label_tries.Text = computedKeys.ToString();
label_bruteforcetxt.Text = currentComputingPass;
if (pass != null)
{
txtPassphrase.Text = pass;
}
}
///
/// Starts the recursive method which will create the keys via brute force
///
/// The length of the key
private void startBruteForce(int keyLength, PassphraseKeyPair ppkp)
{
var keyChars = createCharArray(keyLength, charactersToTest[0]);
// The index of the last character will be stored for slight perfomance improvement
var indexOfLastChar = keyLength - 1;
createNewKey(0, keyChars, keyLength, indexOfLastChar, ppkp);
}
///
/// Creates a new char array of a specific length filled with the defaultChar
///
/// The length of the array
/// The char with whom the array will be filled
///
private char[] createCharArray(int length, char defaultChar)
{
return (from c in new char[length] select defaultChar).ToArray();
}
///
/// This is the main workhorse, it creates new keys and compares them to the password until the password
/// is matched or all keys of the current key length have been checked
///
/// The position of the char which is replaced by new characters currently
/// The current key represented as char array
/// The length of the key
/// The index of the last character of the key
private void createNewKey(int currentCharPosition, char[] keyChars, int keyLength, int indexOfLastChar, PassphraseKeyPair ppkp)
{
int tryingNumber = 0;
var nextCharPosition = currentCharPosition + 1;
// We are looping trough the full length of our charactersToTest array
for (int i = 0; i < charactersToTestLength; i++)
{
/* The character at the currentCharPosition will be replaced by a
* new character from the charactersToTest array => a new key combination will be created */
keyChars[currentCharPosition] = charactersToTest[i];
// The method calls itself recursively until all positions of the key char array have been replaced
if (currentCharPosition < indexOfLastChar)
{
createNewKey(nextCharPosition, keyChars, keyLength, indexOfLastChar, ppkp);
}
else
{
// A new key has been created, remove this counter to improve performance
computedKeys++;
string txt = new String(keyChars);
while (tryingNumber > 4)
{
Thread.Sleep(10);
}
tryingNumber++;
Task.Run(() =>
{
/* The char array will be converted to a string and compared to the password. If the password
* is matched the loop breaks and the password is stored as result. */
currentComputingPass = txt;
System.Diagnostics.Debug.WriteLine(txt);
if (ppkp.DecryptWithPassphrase(txt))
{
if (!passFound)
{
passFound = true;
pass = txt;
}
}
tryingNumber--;
});
if (passFound)
return;
}
}
}
#endregion