/*
* Copyright 2013 bits of proof zrt.
*
* 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.bitsofproof.supernode.wallet;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.crypto.generators.SCrypt;
import com.bitsofproof.supernode.common.ByteUtils;
import com.bitsofproof.supernode.common.ExtendedKey;
import com.bitsofproof.supernode.common.Hash;
import com.bitsofproof.supernode.common.ValidationException;
/**
* BIP of serialized or encrypted HD key root discussion: https://bitcointalk.org/index.php?topic=258678.0
*/
public class EncryptedHDRoot
{
public static enum ScryptDifficulty
{
LOW, MEDIUM, HIGH
}
private static final byte[] clear16 = { 0x0b, 0x2d, 0x7b };
private static final byte[] clear32 = { 0x14, (byte) 0x82, 0x17 };
private static final byte[] clear64 = { 0x01, 0x30, (byte) 0xb7 };
private static final byte[] encrypted16 = { 0x14, (byte) 0xd6, 0x0d };
private static final byte[] encrypted16l = encrypted16;
private static final byte[] encrypted16m = { 0x14, (byte) 0xd6, 0x0e };
private static final byte[] encrypted16h = { 0x14, (byte) 0xd6, 0x0f };
private static final byte[] encrypted32 = { 0x26, 0x3a, (byte) 0xa2 };
private static final byte[] encrypted32l = encrypted32;
private static final byte[] encrypted32m = { 0x26, 0x3a, (byte) 0xa3 };
private static final byte[] encrypted32h = { 0x26, 0x3a, (byte) 0xa4 };
private static final byte[] encrypted64 = { 0x02, 0x38, 0x04 };
private static final byte[] encrypted64l = encrypted64;
private static final byte[] encrypted64m = { 0x02, 0x38, 0x05 };
private static final byte[] encrypted64h = { 0x02, 0x38, 0x06 };
public static Date decodeBirthDate (String ws) throws ValidationException
{
byte[] raw = ByteUtils.fromBase58WithChecksum (ws);
int weeks = raw[3] + raw[4] << 8;
Calendar c = new GregorianCalendar (2013, Calendar.JANUARY, 1);
c.add (Calendar.DAY_OF_YEAR, weeks * 7);
return c.getTime ();
}
public static ExtendedKey decode (String ws) throws ValidationException
{
byte[] raw = ByteUtils.fromBase58WithChecksum (ws);
byte[] magic = Arrays.copyOf (raw, 3);
byte[] seed;
if ( Arrays.equals (magic, clear16) )
{
seed = Arrays.copyOfRange (raw, 9, 16 + 9);
}
else if ( Arrays.equals (magic, clear32) )
{
seed = Arrays.copyOfRange (raw, 9, 32 + 9);
}
else if ( Arrays.equals (magic, clear64) )
{
seed = Arrays.copyOfRange (raw, 9, 64 + 9);
}
else
{
throw new ValidationException ("Not an encoded HD root");
}
ExtendedKey key = ExtendedKey.create (seed);
if ( !Arrays.equals (Arrays.copyOf (Hash.hash (key.getMaster ().getPrivate ()), 4), Arrays.copyOfRange (raw, 5, 9)) )
{
throw new ValidationException ("HD root checksum error");
}
return key;
}
public static ExtendedKey decrypt (String ws, String passphrase) throws ValidationException
{
byte[] raw = ByteUtils.fromBase58WithChecksum (ws);
byte[] magic = Arrays.copyOf (raw, 3);
byte[] encryptedSeed;
int N = 1 << 14;
int r = 16;
int p = 16;
if ( Arrays.equals (magic, encrypted16l) )
{
encryptedSeed = Arrays.copyOfRange (raw, 9, 16 + 9);
r = p = 8;
}
else if ( Arrays.equals (magic, encrypted16m) )
{
encryptedSeed = Arrays.copyOfRange (raw, 9, 16 + 9);
N = 1 << 16;
}
else if ( Arrays.equals (magic, encrypted16h) )
{
encryptedSeed = Arrays.copyOfRange (raw, 9, 16 + 9);
N = 1 << 18;
}
else if ( Arrays.equals (magic, encrypted32l) )
{
encryptedSeed = Arrays.copyOfRange (raw, 9, 32 + 9);
r = p = 8;
}
else if ( Arrays.equals (magic, encrypted32m) )
{
encryptedSeed = Arrays.copyOfRange (raw, 9, 32 + 9);
N = 1 << 16;
}
else if ( Arrays.equals (magic, encrypted32h) )
{
encryptedSeed = Arrays.copyOfRange (raw, 9, 32 + 9);
N = 1 << 18;
}
else if ( Arrays.equals (magic, encrypted64l) )
{
encryptedSeed = Arrays.copyOfRange (raw, 9, 64 + 9);
r = p = 8;
}
else if ( Arrays.equals (magic, encrypted64m) )
{
encryptedSeed = Arrays.copyOfRange (raw, 9, 64 + 9);
N = 1 << 16;
}
else if ( Arrays.equals (magic, encrypted64h) )
{
encryptedSeed = Arrays.copyOfRange (raw, 9, 64 + 9);
N = 1 << 18;
}
else
{
throw new ValidationException ("Not an encoded HD root");
}
byte salt[] = Arrays.copyOf (raw, 9);
Mac mac;
try
{
mac = Mac.getInstance ("HmacSHA512", "BC");
SecretKey seedkey = new SecretKeySpec (salt, "HmacSHA512");
mac.init (seedkey);
byte[] preH = mac.doFinal (passphrase.getBytes ("UTF-8"));
byte[] strongH = SCrypt.generate (preH, preH, N, r, p, 64);
seedkey = new SecretKeySpec (passphrase.getBytes ("UTF-8"), "HmacSHA512");
mac.init (seedkey);
byte[] postH = mac.doFinal (salt);
byte[] H = SCrypt.generate (postH, strongH, 1 << 10, 1, 1, encryptedSeed.length + 32);
byte[] X = Arrays.copyOf (H, encryptedSeed.length);
SecretKeySpec keyspec = new SecretKeySpec (Arrays.copyOfRange (H, encryptedSeed.length, encryptedSeed.length + 32), "AES");
Cipher cipher = Cipher.getInstance ("AES/ECB/NoPadding", "BC");
cipher.init (Cipher.DECRYPT_MODE, keyspec);
byte[] seed = cipher.doFinal (encryptedSeed);
for ( int i = 0; i < encryptedSeed.length; ++i )
{
seed[i] ^= X[i];
}
ExtendedKey key = ExtendedKey.create (seed);
if ( !Arrays.equals (Arrays.copyOf (Hash.hash (key.getMaster ().getPrivate ()), 4), Arrays.copyOfRange (raw, 5, 9)) )
{
throw new ValidationException ("HD root checksum error");
}
return key;
}
catch ( NoSuchAlgorithmException | NoSuchProviderException | InvalidKeyException | UnsupportedEncodingException | NoSuchPaddingException
| IllegalBlockSizeException | BadPaddingException e )
{
throw new ValidationException (e);
}
}
public static String encode (byte[] seed, Date birth) throws ValidationException
{
if ( seed == null || (seed.length != 16 && seed.length != 32 && seed.length != 64) )
{
throw new ValidationException ("Seed must be 16, 32 or 64 bytes");
}
int weeks =
(int) ((birth.getTime () - new GregorianCalendar (2013, Calendar.JANUARY, 1).getTime ().getTime ()) / (7 * 24 * 60 * 60 * 1000L));
ExtendedKey key = ExtendedKey.create (seed);
byte raw[];
if ( seed.length == 16 )
{
raw = new byte[25];
System.arraycopy (clear16, 0, raw, 0, 3);
}
else if ( seed.length == 32 )
{
raw = new byte[41];
System.arraycopy (clear32, 0, raw, 0, 3);
}
else
{
raw = new byte[73];
System.arraycopy (clear64, 0, raw, 0, 3);
}
raw[3] = (byte) (weeks & 0xff);
raw[4] = (byte) ((weeks >>> 8) & 0xff);
System.arraycopy (Hash.hash (key.getMaster ().getPrivate ()), 0, raw, 5, 4);
System.arraycopy (seed, 0, raw, 9, seed.length);
return ByteUtils.toBase58WithChecksum (raw);
}
public static String encrypt (byte[] seed, Date birth, String passphrase, ScryptDifficulty scryptDifficulty) throws ValidationException
{
if ( seed == null || (seed.length != 16 && seed.length != 32 && seed.length != 64) )
{
throw new ValidationException ("Seed must be 16, 32 or 64 bytes");
}
int weeks =
(int) ((birth.getTime () - new GregorianCalendar (2013, Calendar.JANUARY, 1).getTime ().getTime ()) / (7 * 24 * 60 * 60 * 1000L));
ExtendedKey key = ExtendedKey.create (seed);
byte raw[];
byte[] salt = new byte[9];
if ( seed.length == 16 )
{
raw = new byte[25];
System.arraycopy (encrypted16, 0, salt, 0, 3);
}
else if ( seed.length == 32 )
{
raw = new byte[41];
System.arraycopy (encrypted32, 0, salt, 0, 3);
}
else
{
raw = new byte[73];
System.arraycopy (encrypted64, 0, salt, 0, 3);
}
salt[2] += scryptDifficulty.ordinal ();
salt[3] = (byte) (weeks & 0xff);
salt[4] = (byte) ((weeks >>> 8) & 0xff);
System.arraycopy (Hash.hash (key.getMaster ().getPrivate ()), 0, salt, 5, 4);
System.arraycopy (salt, 0, raw, 0, 9);
int N = (1 << 14) << (scryptDifficulty.ordinal () * 2);
int r = scryptDifficulty == ScryptDifficulty.LOW ? 8 : 16;
int p = scryptDifficulty == ScryptDifficulty.LOW ? 8 : 16;
Mac mac;
try
{
mac = Mac.getInstance ("HmacSHA512", "BC");
SecretKey seedkey = new SecretKeySpec (salt, "HmacSHA512");
mac.init (seedkey);
byte[] preH = mac.doFinal (passphrase.getBytes ("UTF-8"));
byte[] strongH = SCrypt.generate (preH, preH, N, r, p, 64);
seedkey = new SecretKeySpec (passphrase.getBytes ("UTF-8"), "HmacSHA512");
mac.init (seedkey);
byte[] postH = mac.doFinal (salt);
byte[] H = SCrypt.generate (postH, strongH, 1 << 10, 1, 1, seed.length + 32);
byte[] X = Arrays.copyOf (H, seed.length);
for ( int i = 0; i < seed.length; ++i )
{
X[i] ^= seed[i];
}
SecretKeySpec keyspec = new SecretKeySpec (Arrays.copyOfRange (H, seed.length, seed.length + 32), "AES");
Cipher cipher = Cipher.getInstance ("AES/ECB/NoPadding", "BC");
cipher.init (Cipher.ENCRYPT_MODE, keyspec);
System.arraycopy (cipher.doFinal (X), 0, raw, 9, seed.length);
}
catch ( NoSuchAlgorithmException | NoSuchProviderException | InvalidKeyException | UnsupportedEncodingException | NoSuchPaddingException
| IllegalBlockSizeException | BadPaddingException e )
{
throw new ValidationException (e);
}
return ByteUtils.toBase58WithChecksum (raw);
}
}