Skip to content

Commit

Permalink
Add the ground work for Armory recovery option
Browse files Browse the repository at this point in the history
  • Loading branch information
Coding-Enthusiast committed Mar 4, 2021
1 parent c12721a commit 8d16100
Showing 1 changed file with 302 additions and 2 deletions.
304 changes: 302 additions & 2 deletions Src/FinderOuter/Services/MnemonicSevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Distributed under the MIT software license, see the accompanying
// file LICENCE or http://www.opensource.org/licenses/mit-license.php.

using Autarkysoft.Bitcoin;
using Autarkysoft.Bitcoin.Cryptography.Asymmetric.EllipticCurve;
using Autarkysoft.Bitcoin.ImprovementProposals;
using FinderOuter.Backend;
Expand All @@ -27,6 +28,7 @@ public enum MnemonicTypes
{
BIP39,
Electrum,
Armory
}

public class MnemonicSevice
Expand Down Expand Up @@ -1376,6 +1378,302 @@ private unsafe void LoopElectrum(ElectrumMnemonic.MnemonicType mnType)



[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe bool MoveNextArmory(byte* items, int len)
{
for (int i = len - 1; i >= 0; --i)
{
items[i] += 1;

if (items[i] == 16)
{
items[i] = 0;
}
else
{
return true;
}
}

return false;
}


private static byte[] ArmoryDecodeAndComputeChecksum(ReadOnlySpan<char> s)
{
byte[] data = new byte[18];
for (int i = 0; i < 32; i += 2)
{
int a = ConstantsFO.ArmoryChars.IndexOf(s[i]);
int b = ConstantsFO.ArmoryChars.IndexOf(s[i + 1]);

Debug.Assert(a >= 0);
Debug.Assert(b >= 0);

data[i / 2] = (byte)(a << 4);
data[i / 2] |= (byte)b;
}

using Sha256Fo sha256 = new Sha256Fo();
byte[] cs = sha256.ComputeHash(data).SubArray(0, 2);
data[16] = cs[0];
data[17] = cs[1];

return data;
}

private static string EncodeArmory(byte[] data)
{
Debug.Assert(data.Length == 18);
char[] result = new char[36];
for (int i = 0; i < result.Length; i += 2)
{
int x = data[i / 2];
result[i] = ConstantsFO.ArmoryChars[x >> 4];
result[i + 1] = ConstantsFO.ArmoryChars[x & 0b1111];
}
return new string(result);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe bool ArmoryLoop(Sha256Fo sha, uint* wPt, uint* hPt, byte* kPt, uint mask, uint comp)
{
wPt[0] = (uint)((kPt[0] << 24) | (kPt[1] << 16) | (kPt[2] << 8) | kPt[3]);
wPt[1] = (uint)((kPt[4] << 24) | (kPt[5] << 16) | (kPt[6] << 8) | kPt[7]);
wPt[2] = (uint)((kPt[8] << 24) | (kPt[9] << 16) | (kPt[10] << 8) | kPt[11]);
wPt[3] = (uint)((kPt[12] << 24) | (kPt[13] << 16) | (kPt[14] << 8) | kPt[15]);
wPt[4] = 0b10000000_00000000_00000000_00000000U;
wPt[5] = 0;
wPt[6] = 0;
wPt[7] = 0;
wPt[8] = 0;
// From 9 to 14 remain 0
wPt[15] = 128;

sha.Init(hPt);
sha.CompressDouble16(hPt, wPt);

return (hPt[0] & mask) == comp;
}

private unsafe void ArmoryLoop(byte[] preComputed, int missCount1, int missCount2,
uint mask1, uint mask2, uint comp1, uint comp2)
{
using Sha256Fo sha = new Sha256Fo();
byte* kPt = stackalloc byte[32 + missingIndexes.Length];
byte* item1 = kPt + 32;
byte* item2 = item1 + missCount1;
fixed (byte* pre = &preComputed[0])
fixed (int* mi = &missingIndexes[0])
fixed (uint* wPt = &sha.w[0], hPt = &sha.hashState[0])
{
do
{
*(Block32*)kPt = *(Block32*)pre;
int mIndex = 0;
for (int i = 0; i < missCount1; i++)
{
int index = mi[mIndex++];
kPt[index / 2] |= (index % 2 == 0) ? (byte)(item1[i] << 4) : item1[i];
}

wPt[0] = (uint)((kPt[0] << 24) | (kPt[1] << 16) | (kPt[2] << 8) | kPt[3]);
wPt[1] = (uint)((kPt[4] << 24) | (kPt[5] << 16) | (kPt[6] << 8) | kPt[7]);
wPt[2] = (uint)((kPt[8] << 24) | (kPt[9] << 16) | (kPt[10] << 8) | kPt[11]);
wPt[3] = (uint)((kPt[12] << 24) | (kPt[13] << 16) | (kPt[14] << 8) | kPt[15]);
wPt[4] = 0b10000000_00000000_00000000_00000000U;
wPt[5] = 0;
wPt[6] = 0;
wPt[7] = 0;
wPt[8] = 0;
// From 9 to 14 remain 0
wPt[15] = 128;

sha.Init(hPt);
sha.CompressDouble16(hPt, wPt);

if ((hPt[0] & mask1) == comp1)
{
int mIndexInternal = mIndex;
do
{
mIndex = mIndexInternal;
for (int i = 0; i < missCount2; i++)
{
int index = mi[mIndex++];
kPt[(index / 2) + 16] |= (index % 2 == 0) ? (byte)(item2[i] << 4) : item2[i];
}

wPt[0] = (uint)((kPt[16] << 24) | (kPt[17] << 16) | (kPt[18] << 8) | kPt[19]);
wPt[1] = (uint)((kPt[20] << 24) | (kPt[21] << 16) | (kPt[22] << 8) | kPt[23]);
wPt[2] = (uint)((kPt[24] << 24) | (kPt[25] << 16) | (kPt[26] << 8) | kPt[27]);
wPt[3] = (uint)((kPt[28] << 24) | (kPt[29] << 16) | (kPt[30] << 8) | kPt[31]);
wPt[4] = 0b10000000_00000000_00000000_00000000U;
wPt[5] = 0;
wPt[6] = 0;
wPt[7] = 0;
wPt[8] = 0;
// From 9 to 14 remain 0
wPt[15] = 128;

sha.Init(hPt);
sha.CompressDouble16(hPt, wPt);

if ((hPt[0] & mask2) == comp2)
{
// TODO: Use the ICompareService here
}
} while (MoveNextArmory(item2, missCount2));

// Checking second line reached the end and failed, item2 must be reset to 0
for (int i = 0; i < missCount2; i++)
{
item2[i] = 0;
}
}
} while (MoveNextArmory(item1, missCount1));
}
}

private async void FindArmory(string mnemonic, char missChar)
{
if (string.IsNullOrWhiteSpace(mnemonic))
report.Fail("Mnemonic can not be null or empty.");
else
{
string[] split = mnemonic.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)
.Select(x => x = x.Replace(" ", ""))
.ToArray();

// Each line is 36 chars or 18 bytes. First 16 bytes are the data, the remaining 2 bytes are checksum
if (split.Any(x => x.Length != 36))
{
report.Fail("Each line has to have 32 characters.");
return;
}
if (split.Length != 2 && split.Length != 4)
{
report.Fail("Armory back ups are 2 or 4 lines.");
return;
}
if (split.Any(line => line.Any(c => !ConstantsFO.ArmoryChars.Contains(c) && c != missChar)))
{
report.Fail("Imput contains invalid characters.");
return;
}

int missCount1 = split[0].Substring(0, 32).Count(c => c == missChar);
int missCount2 = split[1].Substring(0, 32).Count(c => c == missChar);

string csStr1 = split[0].Substring(32, 4);
string csStr2 = split[1].Substring(32, 4);

missCount = missCount1 + missCount2;
if (missCount == 0)
{
if (!csStr1.Contains(missChar) && !csStr2.Contains(missChar))
{
report.AddMessageSafe("The given recovery phrase isn't missing any characters.");
}
else
{
report.AddMessageSafe("The given recovery phrase is only missing the checksum.");
report.FoundAnyResult = true;
report.AddMessageSafe("The key is:");
using Sha256Fo sha256 = new Sha256Fo();
if (!csStr1.Contains(missChar))
{
report.AddMessageSafe(split[0]);
}
else
{
byte[] data = ArmoryDecodeAndComputeChecksum(split[0]);
report.AddMessageSafe(EncodeArmory(data));
}

if (!csStr2.Contains(missChar))
{
report.AddMessageSafe(split[1]);
}
else
{
byte[] data = ArmoryDecodeAndComputeChecksum(split[1]);
report.AddMessageSafe(EncodeArmory(data));
}
}
return;
}

missingIndexes = new int[missCount];

report.AddMessageSafe($"A total of {BigInteger.Pow(16, missCount):n0} phrases should be checked.");

bool skipCS1 = csStr1 == $"{missChar}{missChar}{missChar}{missChar}";
bool skipCS2 = csStr2 == $"{missChar}{missChar}{missChar}{missChar}";
uint cs1 = 0, cs2 = 0, mask1 = 0, mask2 = 0;
if (!skipCS1)
{
for (int i = 0, j = 28; i < 4; i++, j -= 4)
{
if (csStr1[i] != missChar)
{
cs1 |= (uint)ConstantsFO.ArmoryChars.IndexOf(csStr1[i]) << j;
mask1 |= (uint)0xf << j;
}
}
}
if (!skipCS2)
{
for (int i = 0, j = 28; i < 4; i++, j -= 4)
{
if (csStr2[i] != missChar)
{
cs2 |= (uint)ConstantsFO.ArmoryChars.IndexOf(csStr2[i]) << j;
mask2 |= (uint)0xf << j;
}
}
}

// Pre-computation:

byte[] preComp = new byte[32];
int miIndex = 0;
for (int i = 0; i < 2; i++)
{
for (int j = 0; j < 32; j++)
{
int index = ConstantsFO.ArmoryChars.IndexOf(split[i][j]);
if (index < 0)
{
if (i == 0)
missingIndexes[miIndex++] = j;
else
missingIndexes[missCount2 + miIndex++] = j;
}
else
{
int keyIndex = i == 0 ? (j / 2) : (j / 2) + 16;
preComp[keyIndex] |= (j % 2) == 0 ? (byte)(index << 4) : (byte)index;
}
}
miIndex = 0;
}

if (split.Length == 2)
{
// TODO: Set IComprareService based on the given child key/address
}
else if (split.Length == 4)
{
// TODO: Set IComprareService based on chain-code and the given child key/address
}

await Task.Run(() => ArmoryLoop(preComp, missCount1, missCount2, mask1, mask2, cs1, cs2));
}
}



private BigInteger GetTotalCount(int missCount) => BigInteger.Pow(2048, missCount);

public bool TrySetWordList(BIP0039.WordLists wl, out string[] allWords, out int maxWordLen)
Expand Down Expand Up @@ -1485,10 +1783,12 @@ public string GetElectrumPath(ElectrumMnemonic.MnemonicType value)
// TODO: implement Electrum seed recovery with other word lists (they need normalization)
if (mnType == MnemonicTypes.Electrum && wl != BIP0039.WordLists.English)
report.Fail("Only English words are currently supported for Electrum mnemonics.");
else if (!TrySetWordList(wl, out allWords, out int maxWordLen))
report.Fail($"Could not find {wl} word list among resources.");
else if (!inputService.IsMissingCharValid(missChar))
report.Fail("Missing character is not accepted.");
else if (mnType == MnemonicTypes.Armory)
FindArmory(mnemonic, missChar);
else if (!TrySetWordList(wl, out allWords, out int maxWordLen))
report.Fail($"Could not find {wl} word list among resources.");
else if (!TrySplitMnemonic(mnemonic, missChar))
return;
else
Expand Down

0 comments on commit 8d16100

Please sign in to comment.