1 module tagion.wallet.BIP39;
2 
3 import std.string : representation;
4 import tagion.basic.Debug;
5 import tagion.basic.Version : ver;
6 import tagion.crypto.random.random;
7 
8 static assert(ver.LittleEndian, "At the moment bip39 only supports Little Endian");
9 
10 /*
11 https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
12 10001111110100110100110001011001100010111110011101010000101001000000110000011001101010001100001000011101110011000100000111111100
13 0111
14 
15 more omit biology blind insect faith corn crush search unveil away wedding
16 
17 1010010001011010010011001111111000111100011110010110110111110111011110101101101010000000010001010111111001100010011010101000110000110100110011100000010001010001010100110111101110001010011010100000101011000001101000110000010110001001100011100001110011010010
18 01001011
19 
20 
21 picture sponsor display jump nothing wing twin exotic earth vessel one blur erupt acquire earn hunt media expect race ecology flat shove infant enact
22 
23 1137e53d16d7ce04339914da41bdeb24b246f0878494f066a145f8a7f43c8264e177873c54830fa9b0cafdf5846258521b208f6d7fcd0de78ac22bf51040efde
24 */
25 
26 import std.algorithm;
27 import std.range;
28 import std.typecons;
29 import tagion.hibon.HiBONRecord;
30 
31 @safe
32 struct WordList {
33     import tagion.crypto.pbkdf2;
34     import std.digest.sha : SHA512;
35 
36     alias pbkdf2_sha512 = pbkdf2!SHA512;
37     const(ushort[string]) table;
38     const(string[]) words;
39     enum presalt = "mnemonic";
40     this(const(string[]) list) pure nothrow
41     in (list.length == TOTAL_WORDS)
42     do {
43         words = list;
44         table = list
45             .enumerate!ushort
46             .map!(w => tuple!("index", "value")(w.value, w.index))
47             .assocArray;
48 
49     }
50 
51     void gen(ref scope ushort[] words) const nothrow {
52         foreach (ref word; words) {
53             word = getRandom!ushort & (TOTAL_WORDS - 1);
54         }
55     }
56 
57     const(ushort[]) mnemonicNumbers(const(string[]) mnemonics) const pure {
58         return mnemonics
59             .map!(m => table.get(m, ushort.max))
60             .array;
61     }
62 
63     bool checkMnemoicNumbers(scope const(ushort[]) mnemonic_codes) const pure nothrow @nogc {
64         return mnemonic_codes.all!(m => m < TOTAL_WORDS);
65     }
66 
67     ubyte[] opCall(scope const(ushort[]) mnemonic_codes, scope const(char[]) passphrase) const nothrow {
68         scope word_list = mnemonic_codes[]
69             .map!(mnemonic_code => words[mnemonic_code]);
70         return opCall(word_list, passphrase);
71     }
72 
73     char[] passphrase(const uint number_of_words) const nothrow {
74         scope ushort[] mnemonic_codes;
75         mnemonic_codes.length = number_of_words;
76         scope (exit) {
77             mnemonic_codes[] = 0;
78         }
79         gen(mnemonic_codes);
80         const password_size = mnemonic_codes
81             .map!(code => words[code])
82             .map!(m => m.length)
83             .sum + mnemonic_codes.length - 1;
84         auto result = new char[password_size];
85         result[] = ' ';
86         uint index;
87         foreach (code; mnemonic_codes) {
88             result[index .. index + words[code].length] = words[code];
89             index += words[code].length + char.sizeof;
90         }
91         return result;
92     }
93 
94     enum count = 2048;
95     enum dk_length = 64;
96     ubyte[] opCall(R)(scope R mnemonics, scope const(char[]) passphrase) const nothrow if (isInputRange!R) {
97         scope char[] salt = presalt ~ passphrase;
98         const password_size = mnemonics.map!(m => m.length).sum + mnemonics.length - 1;
99         scope password = new char[password_size];
100         scope (exit) {
101             password[] = 0;
102             salt[] = 0;
103         }
104         password[] = ' ';
105         uint index;
106         foreach (mnemonic; mnemonics) {
107             password[index .. index + mnemonic.length] = mnemonic;
108             index += mnemonic.length + char.sizeof;
109         }
110         return pbkdf2_sha512(password.representation, salt.representation, count, dk_length);
111     }
112 
113     enum MAX_WORDS = 24; /// Max number of mnemonic word in a string
114     enum MNEMONIC_BITS = 11; /// Bit size of the word number 2^11=2048
115     enum TOTAL_WORDS = 1 << MNEMONIC_BITS;
116     enum MAX_BITS = MAX_WORDS * MNEMONIC_BITS; /// Total number of bits
117 
118     @trusted
119     ubyte[] entropy(const(ushort[]) mnemonic_codes) const {
120         import std.bitmanip : nativeToBigEndian, nativeToLittleEndian;
121         import std.stdio;
122 
123         const total_bits = mnemonic_codes.length * MNEMONIC_BITS;
124         const total_bytes = total_bits / 8 + ((total_bits & 7) != 0);
125         ubyte[] result = new ubyte[total_bytes];
126 
127         foreach (i, mnemonic; mnemonic_codes) {
128             const bit_pos = i * MNEMONIC_BITS;
129             const byte_pos = bit_pos / 8;
130             const shift_pos = 32 - (11 + (bit_pos & 7));
131             const mnemonic_bytes = (uint(mnemonic) << shift_pos).nativeToBigEndian;
132             result[byte_pos] |= mnemonic_bytes[0];
133             result[byte_pos + 1] = mnemonic_bytes[1];
134             if (mnemonic_bytes[2]) {
135                 result[byte_pos + 2] = mnemonic_bytes[2];
136 
137             }
138         }
139         return result;
140     }
141 
142 }
143 
144 /*
145 https://learnmeabitcoin.com/technical/mnemonic
146 later echo alcohol essence charge eight feel sweet nephew apple aerobic device
147 01111101010010001011110000011000001001101010001001101000100011011101010100110110110111011001010001000001010101000001000010011110
148 0101
149 */
150 
151 @safe
152 unittest {
153     //    import std.stdio;
154     import tagion.wallet.bip39_english;
155     import std.format;
156     import std.string : representation;
157 
158     const wordlist = WordList(words);
159     {
160         const mnemonic = [
161             "punch",
162             "shock",
163             "entire",
164             "north",
165             "file",
166             "identify"
167         ];
168         const(ushort[]) expected_mnemonic_codes = [1390, 1586, 604, 1202, 689, 900];
169         immutable expected_entropy = "101011011101100011001001001011100100101100100101011000101110000100";
170         const mnemonic_codes = wordlist.mnemonicNumbers(mnemonic);
171         assert(expected_mnemonic_codes == mnemonic_codes);
172         string mnemonic_codes_bits = format("%(%011b%)", mnemonic_codes);
173         assert(expected_entropy == mnemonic_codes_bits);
174         const entropy = wordlist.entropy(mnemonic_codes);
175         string entropy_bits = format("%(%08b%)", entropy)[0 .. 11 * expected_mnemonic_codes.length];
176         assert(expected_entropy == entropy_bits);
177     }
178     {
179         const mnemonic = [
180             "later",
181             "echo",
182             "alcohol",
183             "essence",
184             "charge",
185             "eight",
186             "feel",
187             "sweet",
188             "nephew",
189             "apple",
190             "aerobic",
191             "device"
192         ];
193         //        const(ushort[]) mnemonic_code =[1390, 1586, 604, 1202, 689, 900];
194         immutable expected_entropy = "011111010100100010111100000110000010011010100010011010001000110111010101001101101101110110010100010000010101010000010000100111100101";
195         const mnemonic_codes = wordlist.mnemonicNumbers(mnemonic);
196         string entropy_bits = format("%(%011b%)", wordlist.mnemonicNumbers(mnemonic));
197         assert(expected_entropy == entropy_bits);
198 
199     }
200     { /// PBKDF2 BIP39
201         const mnemonic = [
202             "basket",
203             "actual"
204         ];
205 
206         import tagion.crypto.pbkdf2;
207         import std.bitmanip : nativeToBigEndian;
208         import std.digest.sha : SHA512;
209 
210         const mnemonic_codes = wordlist.mnemonicNumbers(mnemonic);
211 
212         const entropy = wordlist.entropy(mnemonic_codes);
213         string mnemonic_codes_bits = format("%(%011b%)", mnemonic_codes);
214         string entropy_bits = format("%(%08b%)", entropy); //[0 .. 12 * mnemonic_codes.length];
215         alias pbkdf2_sha512 = pbkdf2!SHA512;
216         string salt = "mnemonic"; //.representation;
217         const entropy1 = "basket actual".representation;
218         const result1 = pbkdf2_sha512(entropy1, salt.representation, 2048, 64);
219     }
220 }