Skip to content

Commit 88dfb35

Browse files
committed
Added serialized or encrypted HD key root following
https://bitcointalk.org/index.php?topic=258678.0
1 parent 0c6ce4b commit 88dfb35

3 files changed

Lines changed: 432 additions & 0 deletions

File tree

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
/*
2+
* Copyright 2013 bits of proof zrt.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.bitsofproof.supernode.wallet;
17+
18+
import java.io.UnsupportedEncodingException;
19+
import java.security.InvalidKeyException;
20+
import java.security.NoSuchAlgorithmException;
21+
import java.security.NoSuchProviderException;
22+
import java.util.Arrays;
23+
import java.util.Calendar;
24+
import java.util.Date;
25+
import java.util.GregorianCalendar;
26+
27+
import javax.crypto.BadPaddingException;
28+
import javax.crypto.Cipher;
29+
import javax.crypto.IllegalBlockSizeException;
30+
import javax.crypto.Mac;
31+
import javax.crypto.NoSuchPaddingException;
32+
import javax.crypto.SecretKey;
33+
import javax.crypto.spec.SecretKeySpec;
34+
35+
import org.bouncycastle.crypto.generators.SCrypt;
36+
37+
import com.bitsofproof.supernode.common.ByteUtils;
38+
import com.bitsofproof.supernode.common.ExtendedKey;
39+
import com.bitsofproof.supernode.common.Hash;
40+
import com.bitsofproof.supernode.common.ValidationException;
41+
42+
/**
43+
* BIP of serialized or encrypted HD key root discussion: https://bitcointalk.org/index.php?topic=258678.0
44+
*/
45+
public class EncryptedHDRoot
46+
{
47+
public static enum ScryptDifficulty
48+
{
49+
LOW, MEDIUM, HIGH
50+
}
51+
52+
private static final byte[] clear16 = { 0x0b, 0x2d, 0x7b };
53+
private static final byte[] clear32 = { 0x14, (byte) 0x82, 0x17 };
54+
private static final byte[] clear64 = { 0x01, 0x30, (byte) 0xb7 };
55+
private static final byte[] encrypted16 = { 0x14, (byte) 0xd6, 0x0d };
56+
private static final byte[] encrypted16l = encrypted16;
57+
private static final byte[] encrypted16m = { 0x14, (byte) 0xd6, 0x0e };
58+
private static final byte[] encrypted16h = { 0x14, (byte) 0xd6, 0x0f };
59+
private static final byte[] encrypted32 = { 0x26, 0x3a, (byte) 0xa2 };
60+
private static final byte[] encrypted32l = encrypted32;
61+
private static final byte[] encrypted32m = { 0x26, 0x3a, (byte) 0xa3 };
62+
private static final byte[] encrypted32h = { 0x26, 0x3a, (byte) 0xa4 };
63+
private static final byte[] encrypted64 = { 0x02, 0x38, 0x04 };
64+
private static final byte[] encrypted64l = encrypted64;
65+
private static final byte[] encrypted64m = { 0x02, 0x38, 0x05 };
66+
private static final byte[] encrypted64h = { 0x02, 0x38, 0x06 };
67+
68+
public Date decodeBirthDate (String ws) throws ValidationException
69+
{
70+
byte[] raw = ByteUtils.fromBase58WithChecksum (ws);
71+
int weeks = raw[3] + raw[4] << 8;
72+
Calendar c = new GregorianCalendar (2013, Calendar.JANUARY, 1);
73+
c.add (Calendar.DAY_OF_YEAR, weeks * 7);
74+
return c.getTime ();
75+
}
76+
77+
public static ExtendedKey decode (String ws) throws ValidationException
78+
{
79+
byte[] raw = ByteUtils.fromBase58WithChecksum (ws);
80+
byte[] magic = Arrays.copyOf (raw, 3);
81+
byte[] seed;
82+
if ( Arrays.equals (magic, clear16) )
83+
{
84+
seed = Arrays.copyOfRange (raw, 9, 16 + 9);
85+
}
86+
else if ( Arrays.equals (magic, clear32) )
87+
{
88+
seed = Arrays.copyOfRange (raw, 9, 32 + 9);
89+
}
90+
else if ( Arrays.equals (magic, clear64) )
91+
{
92+
seed = Arrays.copyOfRange (raw, 9, 64 + 9);
93+
}
94+
else
95+
{
96+
throw new ValidationException ("Not an encoded HD root");
97+
}
98+
ExtendedKey key = ExtendedKey.create (seed);
99+
if ( !Arrays.equals (Arrays.copyOf (Hash.hash (key.getMaster ().getPrivate ()), 4), Arrays.copyOfRange (raw, 5, 9)) )
100+
{
101+
throw new ValidationException ("HD root checksum error");
102+
}
103+
return key;
104+
}
105+
106+
public static ExtendedKey decrypt (String ws, String passphrase) throws ValidationException
107+
{
108+
byte[] raw = ByteUtils.fromBase58WithChecksum (ws);
109+
byte[] magic = Arrays.copyOf (raw, 3);
110+
byte[] encryptedSeed;
111+
int N = 1 << 14;
112+
int r = 16;
113+
int p = 16;
114+
if ( Arrays.equals (magic, encrypted16l) )
115+
{
116+
encryptedSeed = Arrays.copyOfRange (raw, 9, 16 + 9);
117+
r = p = 8;
118+
}
119+
else if ( Arrays.equals (magic, encrypted16m) )
120+
{
121+
encryptedSeed = Arrays.copyOfRange (raw, 9, 16 + 9);
122+
N = 1 << 16;
123+
}
124+
else if ( Arrays.equals (magic, encrypted16h) )
125+
{
126+
encryptedSeed = Arrays.copyOfRange (raw, 9, 16 + 9);
127+
N = 1 << 18;
128+
}
129+
else if ( Arrays.equals (magic, encrypted32l) )
130+
{
131+
encryptedSeed = Arrays.copyOfRange (raw, 9, 32 + 9);
132+
r = p = 8;
133+
}
134+
else if ( Arrays.equals (magic, encrypted32m) )
135+
{
136+
encryptedSeed = Arrays.copyOfRange (raw, 9, 32 + 9);
137+
N = 1 << 16;
138+
}
139+
else if ( Arrays.equals (magic, encrypted32h) )
140+
{
141+
encryptedSeed = Arrays.copyOfRange (raw, 9, 32 + 9);
142+
N = 1 << 18;
143+
}
144+
else if ( Arrays.equals (magic, encrypted64l) )
145+
{
146+
encryptedSeed = Arrays.copyOfRange (raw, 9, 64 + 9);
147+
r = p = 8;
148+
}
149+
else if ( Arrays.equals (magic, encrypted64m) )
150+
{
151+
encryptedSeed = Arrays.copyOfRange (raw, 9, 64 + 9);
152+
N = 1 << 16;
153+
}
154+
else if ( Arrays.equals (magic, encrypted64h) )
155+
{
156+
encryptedSeed = Arrays.copyOfRange (raw, 9, 64 + 9);
157+
N = 1 << 18;
158+
}
159+
else
160+
{
161+
throw new ValidationException ("Not an encoded HD root");
162+
}
163+
byte salt[] = Arrays.copyOf (raw, 9);
164+
165+
Mac mac;
166+
try
167+
{
168+
mac = Mac.getInstance ("HmacSHA512", "BC");
169+
SecretKey seedkey = new SecretKeySpec (salt, "HmacSHA512");
170+
mac.init (seedkey);
171+
byte[] preH = mac.doFinal (passphrase.getBytes ("UTF-8"));
172+
byte[] strongH = SCrypt.generate (preH, preH, N, r, p, 64);
173+
seedkey = new SecretKeySpec (passphrase.getBytes ("UTF-8"), "HmacSHA512");
174+
mac.init (seedkey);
175+
byte[] postH = mac.doFinal (salt);
176+
byte[] H = SCrypt.generate (postH, strongH, 1 << 10, 1, 1, encryptedSeed.length + 32);
177+
byte[] X = Arrays.copyOf (H, encryptedSeed.length);
178+
SecretKeySpec keyspec = new SecretKeySpec (Arrays.copyOfRange (H, encryptedSeed.length, encryptedSeed.length + 32), "AES");
179+
Cipher cipher = Cipher.getInstance ("AES/ECB/NoPadding", "BC");
180+
cipher.init (Cipher.DECRYPT_MODE, keyspec);
181+
byte[] seed = cipher.doFinal (encryptedSeed);
182+
for ( int i = 0; i < encryptedSeed.length; ++i )
183+
{
184+
seed[i] ^= X[i];
185+
}
186+
ExtendedKey key = ExtendedKey.create (seed);
187+
if ( !Arrays.equals (Arrays.copyOf (Hash.hash (key.getMaster ().getPrivate ()), 4), Arrays.copyOfRange (raw, 5, 9)) )
188+
{
189+
throw new ValidationException ("HD root checksum error");
190+
}
191+
return key;
192+
}
193+
catch ( NoSuchAlgorithmException | NoSuchProviderException | InvalidKeyException | UnsupportedEncodingException | NoSuchPaddingException
194+
| IllegalBlockSizeException | BadPaddingException e )
195+
{
196+
throw new ValidationException (e);
197+
}
198+
}
199+
200+
public static String encode (byte[] seed, Date birth) throws ValidationException
201+
{
202+
if ( seed == null || (seed.length != 16 && seed.length != 32 && seed.length != 64) )
203+
{
204+
throw new ValidationException ("Seed must be 16, 32 or 64 bytes");
205+
}
206+
int weeks =
207+
(int) ((birth.getTime () - new GregorianCalendar (2013, Calendar.JANUARY, 1).getTime ().getTime ()) / (7 * 24 * 60 * 60 * 1000L));
208+
209+
ExtendedKey key = ExtendedKey.create (seed);
210+
byte raw[];
211+
if ( seed.length == 16 )
212+
{
213+
raw = new byte[25];
214+
System.arraycopy (clear16, 0, raw, 0, 3);
215+
}
216+
else if ( seed.length == 32 )
217+
{
218+
raw = new byte[41];
219+
System.arraycopy (clear32, 0, raw, 0, 3);
220+
}
221+
else
222+
{
223+
raw = new byte[73];
224+
System.arraycopy (clear64, 0, raw, 0, 3);
225+
}
226+
raw[3] = (byte) (weeks & 0xff);
227+
raw[4] = (byte) ((weeks >>> 8) & 0xff);
228+
System.arraycopy (Hash.hash (key.getMaster ().getPrivate ()), 0, raw, 5, 4);
229+
System.arraycopy (seed, 0, raw, 9, seed.length);
230+
return ByteUtils.toBase58WithChecksum (raw);
231+
}
232+
233+
public static String encrypt (byte[] seed, Date birth, String passphrase, ScryptDifficulty scryptDifficulty) throws ValidationException
234+
{
235+
if ( seed == null || (seed.length != 16 && seed.length != 32 && seed.length != 64) )
236+
{
237+
throw new ValidationException ("Seed must be 16, 32 or 64 bytes");
238+
}
239+
int weeks =
240+
(int) ((birth.getTime () - new GregorianCalendar (2013, Calendar.JANUARY, 1).getTime ().getTime ()) / (7 * 24 * 60 * 60 * 1000L));
241+
242+
ExtendedKey key = ExtendedKey.create (seed);
243+
byte raw[];
244+
byte[] salt = new byte[9];
245+
if ( seed.length == 16 )
246+
{
247+
raw = new byte[25];
248+
System.arraycopy (encrypted16, 0, salt, 0, 3);
249+
}
250+
else if ( seed.length == 32 )
251+
{
252+
raw = new byte[41];
253+
System.arraycopy (encrypted32, 0, salt, 0, 3);
254+
}
255+
else
256+
{
257+
raw = new byte[73];
258+
System.arraycopy (encrypted64, 0, salt, 0, 3);
259+
}
260+
salt[2] += scryptDifficulty.ordinal ();
261+
salt[3] = (byte) (weeks & 0xff);
262+
salt[4] = (byte) ((weeks >>> 8) & 0xff);
263+
System.arraycopy (Hash.hash (key.getMaster ().getPrivate ()), 0, salt, 5, 4);
264+
System.arraycopy (salt, 0, raw, 0, 9);
265+
int N = (1 << 14) << (scryptDifficulty.ordinal () * 2);
266+
int r = scryptDifficulty == ScryptDifficulty.LOW ? 8 : 16;
267+
int p = scryptDifficulty == ScryptDifficulty.LOW ? 8 : 16;
268+
Mac mac;
269+
try
270+
{
271+
mac = Mac.getInstance ("HmacSHA512", "BC");
272+
SecretKey seedkey = new SecretKeySpec (salt, "HmacSHA512");
273+
mac.init (seedkey);
274+
byte[] preH = mac.doFinal (passphrase.getBytes ("UTF-8"));
275+
byte[] strongH = SCrypt.generate (preH, preH, N, r, p, 64);
276+
seedkey = new SecretKeySpec (passphrase.getBytes ("UTF-8"), "HmacSHA512");
277+
mac.init (seedkey);
278+
byte[] postH = mac.doFinal (salt);
279+
byte[] H = SCrypt.generate (postH, strongH, 1 << 10, 1, 1, seed.length + 32);
280+
byte[] X = Arrays.copyOf (H, seed.length);
281+
for ( int i = 0; i < seed.length; ++i )
282+
{
283+
X[i] ^= seed[i];
284+
}
285+
SecretKeySpec keyspec = new SecretKeySpec (Arrays.copyOfRange (H, seed.length, seed.length + 32), "AES");
286+
Cipher cipher = Cipher.getInstance ("AES/ECB/NoPadding", "BC");
287+
cipher.init (Cipher.ENCRYPT_MODE, keyspec);
288+
System.arraycopy (cipher.doFinal (X), 0, raw, 9, seed.length);
289+
}
290+
catch ( NoSuchAlgorithmException | NoSuchProviderException | InvalidKeyException | UnsupportedEncodingException | NoSuchPaddingException
291+
| IllegalBlockSizeException | BadPaddingException e )
292+
{
293+
throw new ValidationException (e);
294+
}
295+
return ByteUtils.toBase58WithChecksum (raw);
296+
}
297+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.bitsofproof.supernode.api;
2+
3+
import static org.junit.Assert.assertTrue;
4+
5+
import java.io.IOException;
6+
import java.io.InputStream;
7+
import java.security.Security;
8+
import java.text.DateFormat;
9+
import java.text.ParseException;
10+
import java.text.SimpleDateFormat;
11+
import java.util.Date;
12+
13+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
14+
import org.json.JSONArray;
15+
import org.json.JSONException;
16+
import org.json.JSONObject;
17+
import org.junit.BeforeClass;
18+
import org.junit.Test;
19+
20+
import com.bitsofproof.supernode.common.ByteUtils;
21+
import com.bitsofproof.supernode.common.ExtendedKey;
22+
import com.bitsofproof.supernode.common.ValidationException;
23+
import com.bitsofproof.supernode.wallet.EncryptedHDRoot;
24+
import com.bitsofproof.supernode.wallet.EncryptedHDRoot.ScryptDifficulty;
25+
26+
public class EncryptedHDRootTest
27+
{
28+
@BeforeClass
29+
public static void init ()
30+
{
31+
Security.addProvider (new BouncyCastleProvider ());
32+
}
33+
34+
private static final String TESTS = "EncryptedHDRoot.json";
35+
36+
private JSONArray readArray (String resource) throws IOException, JSONException
37+
{
38+
InputStream input = this.getClass ().getResource ("/" + resource).openStream ();
39+
StringBuffer content = new StringBuffer ();
40+
byte[] buffer = new byte[1024];
41+
int len;
42+
while ( (len = input.read (buffer)) > 0 )
43+
{
44+
byte[] s = new byte[len];
45+
System.arraycopy (buffer, 0, s, 0, len);
46+
content.append (new String (buffer, "UTF-8"));
47+
}
48+
return new JSONArray (content.toString ());
49+
}
50+
51+
/**
52+
* @throws IOException
53+
* @throws JSONException
54+
* @throws ValidationException
55+
* @throws ParseException
56+
*/
57+
@Test
58+
public void testHDRoot () throws IOException, JSONException, ValidationException, ParseException
59+
{
60+
DateFormat dateFormat = new SimpleDateFormat ("dd-MM-yyyy");
61+
JSONArray testData = readArray (TESTS);
62+
for ( int i = 0; i < testData.length (); ++i )
63+
{
64+
JSONObject test = testData.getJSONObject (i);
65+
byte[] seed = ByteUtils.fromHex (test.getString ("seed"));
66+
Date birth = dateFormat.parse (test.getString ("birth"));
67+
ExtendedKey key = ExtendedKey.create (seed);
68+
assertTrue (key.getMaster ().getAddress ().toString ().equals (test.getString ("address")));
69+
assertTrue (key.serialize (true).equals (test.getString ("private")));
70+
assertTrue (key.getReadOnly ().serialize (true).equals (test.getString ("public")));
71+
assertTrue (EncryptedHDRoot.encode (seed, birth).equals (test.getString ("clear")));
72+
assertTrue (EncryptedHDRoot.encrypt (seed, birth, test.getString ("password"), ScryptDifficulty.LOW).equals (test.getString ("encryptedLow")));
73+
assertTrue (EncryptedHDRoot.encrypt (seed, birth, test.getString ("password"), ScryptDifficulty.MEDIUM).equals (test.getString
74+
("encryptedMedium")));
75+
assertTrue (EncryptedHDRoot.encrypt (seed, birth, test.getString ("password"), ScryptDifficulty.HIGH).equals (test.getString ("encryptedHigh")));
76+
assertTrue (EncryptedHDRoot.decode (test.getString ("clear")).serialize (true).equals (key.serialize (true)));
77+
assertTrue (EncryptedHDRoot.decrypt (test.getString ("encryptedLow"), test.getString ("password")).serialize (true).equals (key.serialize
78+
(true)));
79+
assertTrue (EncryptedHDRoot.decrypt (test.getString ("encryptedMedium"), test.getString ("password")).serialize (true)
80+
.equals (key.serialize (true)));
81+
assertTrue (EncryptedHDRoot.decrypt (test.getString ("encryptedHigh"), test.getString ("password")).serialize (true)
82+
.equals (key.serialize (true)));
83+
}
84+
}
85+
}

0 commit comments

Comments
 (0)