|
|
@ -1,10 +1,40 @@ |
|
|
|
package com.breadwallet.wallet; |
|
|
|
|
|
|
|
import android.app.Activity; |
|
|
|
import android.app.KeyguardManager; |
|
|
|
import android.content.Context; |
|
|
|
import android.security.KeyPairGeneratorSpec; |
|
|
|
import android.util.Log; |
|
|
|
|
|
|
|
import java.io.File; |
|
|
|
import java.io.FileInputStream; |
|
|
|
import java.io.FileOutputStream; |
|
|
|
import java.io.IOException; |
|
|
|
import java.math.BigInteger; |
|
|
|
import java.nio.ByteBuffer; |
|
|
|
import java.security.InvalidAlgorithmParameterException; |
|
|
|
import java.security.InvalidKeyException; |
|
|
|
import java.security.KeyPair; |
|
|
|
import java.security.KeyPairGenerator; |
|
|
|
import java.security.KeyStore; |
|
|
|
import java.security.KeyStoreException; |
|
|
|
import java.security.NoSuchAlgorithmException; |
|
|
|
import java.security.NoSuchProviderException; |
|
|
|
import java.security.SecureRandom; |
|
|
|
import java.security.UnrecoverableEntryException; |
|
|
|
import java.security.cert.CertificateException; |
|
|
|
import java.security.interfaces.RSAPrivateKey; |
|
|
|
import java.security.interfaces.RSAPublicKey; |
|
|
|
import java.text.NumberFormat; |
|
|
|
import java.util.Calendar; |
|
|
|
import java.util.List; |
|
|
|
|
|
|
|
import javax.crypto.Cipher; |
|
|
|
import javax.crypto.CipherInputStream; |
|
|
|
import javax.crypto.CipherOutputStream; |
|
|
|
import javax.crypto.NoSuchPaddingException; |
|
|
|
import javax.security.auth.x500.X500Principal; |
|
|
|
|
|
|
|
/** |
|
|
|
* BreadWallet |
|
|
|
* <p/> |
|
|
@ -45,6 +75,8 @@ public class BRWalletManager { |
|
|
|
public static final String FEE_PER_KB_URL = "https://api.breadwallet.com/v1/fee-per-kb"; |
|
|
|
public static final int SEED_ENTROPY_LENGTH = 128 / 8; |
|
|
|
public static final String SEC_ATTR_SERVICE = "org.voisine.breadwallet"; |
|
|
|
public static final String ANDROID_KEY_STORE = "AndroidKeyStore"; |
|
|
|
public static final String ALIAS = "phrase"; |
|
|
|
|
|
|
|
ByteBuffer masterPublicKey; // master public key used to generate wallet addresses
|
|
|
|
char[] seedPhrase; // requesting seedPhrase will trigger authentication
|
|
|
@ -78,10 +110,11 @@ public class BRWalletManager { |
|
|
|
} |
|
|
|
|
|
|
|
public static boolean setKeychainData(ByteBuffer buffer, String key, boolean authenticated) { |
|
|
|
return false; |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
public static ByteBuffer getKeychainData(String key) { |
|
|
|
public ByteBuffer getKeychainData(String key) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
@ -89,30 +122,167 @@ public class BRWalletManager { |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
public static long getKeychainInt(String key) { |
|
|
|
public long getKeychainInt(String key) { |
|
|
|
return 0; |
|
|
|
} |
|
|
|
|
|
|
|
public static boolean setKeychainString(String s, String key, boolean authenticated) { |
|
|
|
return false; |
|
|
|
public boolean setKeyStoreString(String strPhrase, String key, |
|
|
|
boolean authenticated, Context ctx) { |
|
|
|
try { |
|
|
|
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE); |
|
|
|
keyStore.load(null); |
|
|
|
|
|
|
|
int nBefore = keyStore.size(); |
|
|
|
|
|
|
|
// Create the keys if necessary
|
|
|
|
if (!keyStore.containsAlias(ALIAS)) { |
|
|
|
|
|
|
|
Calendar notBefore = Calendar.getInstance(); |
|
|
|
Calendar notAfter = Calendar.getInstance(); |
|
|
|
notAfter.add(Calendar.YEAR, 1); |
|
|
|
|
|
|
|
KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(ctx) |
|
|
|
.setAlias(ALIAS) |
|
|
|
.setKeyType("RSA") |
|
|
|
.setKeySize(2048) |
|
|
|
.setSubject(new X500Principal("CN=test")) |
|
|
|
.setSerialNumber(BigInteger.ONE) |
|
|
|
.setStartDate(notBefore.getTime()) |
|
|
|
.setEndDate(notAfter.getTime()) |
|
|
|
.build(); |
|
|
|
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", ANDROID_KEY_STORE); |
|
|
|
generator.initialize(spec); |
|
|
|
|
|
|
|
KeyPair keyPair = generator.generateKeyPair(); |
|
|
|
} |
|
|
|
int nAfter = keyStore.size(); |
|
|
|
Log.v(TAG, "Before = " + nBefore + " After = " + nAfter); |
|
|
|
|
|
|
|
// Retrieve the keys
|
|
|
|
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(ALIAS, null); |
|
|
|
RSAPrivateKey privateKey = (RSAPrivateKey) privateKeyEntry.getPrivateKey(); |
|
|
|
RSAPublicKey publicKey = (RSAPublicKey) privateKeyEntry.getCertificate().getPublicKey(); |
|
|
|
|
|
|
|
Log.v(TAG, "private key = " + privateKey.toString()); |
|
|
|
Log.v(TAG, "public key = " + publicKey.toString()); |
|
|
|
|
|
|
|
// Encrypt the text
|
|
|
|
String dataDirectory = ctx.getApplicationInfo().dataDir; |
|
|
|
String filesDirectory = ctx.getFilesDir().getAbsolutePath(); |
|
|
|
String encryptedDataFilePath = filesDirectory + File.separator + "my_phrase"; |
|
|
|
|
|
|
|
Log.v(TAG, "strPhrase = " + strPhrase); |
|
|
|
Log.v(TAG, "dataDirectory = " + dataDirectory); |
|
|
|
Log.v(TAG, "filesDirectory = " + filesDirectory); |
|
|
|
Log.v(TAG, "encryptedDataFilePath = " + encryptedDataFilePath); |
|
|
|
|
|
|
|
Cipher inCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL"); |
|
|
|
inCipher.init(Cipher.ENCRYPT_MODE, publicKey); |
|
|
|
|
|
|
|
CipherOutputStream cipherOutputStream = |
|
|
|
new CipherOutputStream( |
|
|
|
new FileOutputStream(encryptedDataFilePath), inCipher); |
|
|
|
cipherOutputStream.write(strPhrase.getBytes("UTF-8")); |
|
|
|
cipherOutputStream.close(); |
|
|
|
|
|
|
|
} catch (NoSuchAlgorithmException e) { |
|
|
|
Log.e(TAG, Log.getStackTraceString(e)); |
|
|
|
} catch (NoSuchProviderException e) { |
|
|
|
Log.e(TAG, Log.getStackTraceString(e)); |
|
|
|
} catch (InvalidAlgorithmParameterException e) { |
|
|
|
Log.e(TAG, Log.getStackTraceString(e)); |
|
|
|
} catch (KeyStoreException e) { |
|
|
|
Log.e(TAG, Log.getStackTraceString(e)); |
|
|
|
} catch (CertificateException e) { |
|
|
|
Log.e(TAG, Log.getStackTraceString(e)); |
|
|
|
} catch (IOException e) { |
|
|
|
Log.e(TAG, Log.getStackTraceString(e)); |
|
|
|
} catch (UnrecoverableEntryException e) { |
|
|
|
Log.e(TAG, Log.getStackTraceString(e)); |
|
|
|
} catch (NoSuchPaddingException e) { |
|
|
|
Log.e(TAG, Log.getStackTraceString(e)); |
|
|
|
} catch (InvalidKeyException e) { |
|
|
|
Log.e(TAG, Log.getStackTraceString(e)); |
|
|
|
} catch (UnsupportedOperationException e) { |
|
|
|
Log.e(TAG, Log.getStackTraceString(e)); |
|
|
|
} |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
public static String getKeychainString(String key) { |
|
|
|
return null; |
|
|
|
public String getKeyStoreString(String key, Context ctx) { |
|
|
|
KeyStore keyStore; |
|
|
|
String recoveredSecret = ""; |
|
|
|
String filesDirectory = ctx.getFilesDir().getAbsolutePath(); |
|
|
|
String encryptedDataFilePath = filesDirectory + File.separator + "my_phrase"; |
|
|
|
try { |
|
|
|
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE); |
|
|
|
keyStore.load(null); |
|
|
|
|
|
|
|
// Retrieve the keys
|
|
|
|
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) |
|
|
|
keyStore.getEntry(ALIAS, null); |
|
|
|
RSAPrivateKey privateKey = (RSAPrivateKey) privateKeyEntry.getPrivateKey(); |
|
|
|
|
|
|
|
Cipher outCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL"); |
|
|
|
outCipher.init(Cipher.DECRYPT_MODE, privateKey); |
|
|
|
|
|
|
|
CipherInputStream cipherInputStream = new CipherInputStream( |
|
|
|
new FileInputStream(encryptedDataFilePath), outCipher); |
|
|
|
byte[] roundTrippedBytes = new byte[1000]; // TODO: dynamically resize as we get more data
|
|
|
|
int index = 0; |
|
|
|
int nextByte; |
|
|
|
while ((nextByte = cipherInputStream.read()) != -1) { |
|
|
|
roundTrippedBytes[index] = (byte) nextByte; |
|
|
|
index++; |
|
|
|
} |
|
|
|
recoveredSecret = new String(roundTrippedBytes, 0, index, "UTF-8"); |
|
|
|
Log.e(TAG, "round tripped string = " + recoveredSecret); |
|
|
|
} catch (KeyStoreException e) { |
|
|
|
e.printStackTrace(); |
|
|
|
} catch (UnrecoverableEntryException e) { |
|
|
|
e.printStackTrace(); |
|
|
|
} catch (NoSuchAlgorithmException e) { |
|
|
|
e.printStackTrace(); |
|
|
|
} catch (CertificateException e) { |
|
|
|
e.printStackTrace(); |
|
|
|
} catch (IOException e) { |
|
|
|
e.printStackTrace(); |
|
|
|
} catch (NoSuchProviderException e) { |
|
|
|
e.printStackTrace(); |
|
|
|
} catch (NoSuchPaddingException e) { |
|
|
|
e.printStackTrace(); |
|
|
|
} catch (InvalidKeyException e) { |
|
|
|
e.printStackTrace(); |
|
|
|
} catch (NullPointerException e) { |
|
|
|
e.printStackTrace(); |
|
|
|
} |
|
|
|
|
|
|
|
Log.e(TAG, "recovered: " + recoveredSecret); |
|
|
|
return recoveredSecret; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* generates a random seed, saves to keychain and returns the associated seedPhrase |
|
|
|
*/ |
|
|
|
public String generateRandomSeed() { |
|
|
|
public String generateRandomSeed(Context ctx) { |
|
|
|
final SecureRandom sr = new SecureRandom(); |
|
|
|
final byte[] keyBytes = new byte[128]; |
|
|
|
sr.nextBytes(keyBytes); |
|
|
|
|
|
|
|
return null; |
|
|
|
Log.e(TAG, keyBytes.toString()); |
|
|
|
// byte[] wordListBytes = new byte[0];
|
|
|
|
// try {
|
|
|
|
// wordListBytes = WordsReader.getWordListBytes(IntroActivity.app);
|
|
|
|
// } catch (IOException e) {
|
|
|
|
// e.printStackTrace();
|
|
|
|
// }
|
|
|
|
// String phrase = encodePhrase(keyBytes, wordListBytes); //not working yet
|
|
|
|
String phrase = "short apple trunk riot coyote innocent zebra venture ill lava shop test"; |
|
|
|
boolean success = setKeyStoreString(phrase, null, true, ctx); |
|
|
|
Log.e(TAG, "setKeyStoreString was successful: " + success); |
|
|
|
return phrase; |
|
|
|
} |
|
|
|
|
|
|
|
public native String encodePhrase(byte[] seed, byte[] wordList); |
|
|
|
private native String encodePhrase(byte[] seed, byte[] wordList); |
|
|
|
|
|
|
|
/** |
|
|
|
* authenticates user and returns seed |
|
|
@ -146,8 +316,24 @@ public class BRWalletManager { |
|
|
|
* given a private key, queries chain.com for unspent outputs and calls the completion block with |
|
|
|
* a signed transaction that will sweep the balance into wallet (doesn't publish the tx) |
|
|
|
*/ |
|
|
|
public void sweepPrivateKey(String privKey, boolean fee) { |
|
|
|
//?????????????????????????????
|
|
|
|
public void sweepPrivateKey(String privKey, boolean fee, Context ctx) { |
|
|
|
KeyStore keyStore; |
|
|
|
String filesDirectory = ctx.getFilesDir().getAbsolutePath(); |
|
|
|
String encryptedDataFilePath = filesDirectory + File.separator + "my_phrase"; |
|
|
|
try { |
|
|
|
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE); |
|
|
|
keyStore.load(null); |
|
|
|
keyStore.deleteEntry(ALIAS); |
|
|
|
|
|
|
|
} catch (CertificateException e) { |
|
|
|
e.printStackTrace(); |
|
|
|
} catch (NoSuchAlgorithmException e) { |
|
|
|
e.printStackTrace(); |
|
|
|
} catch (KeyStoreException e) { |
|
|
|
e.printStackTrace(); |
|
|
|
} catch (IOException e) { |
|
|
|
e.printStackTrace(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public long amountForString(String string) { |
|
|
@ -169,8 +355,10 @@ public class BRWalletManager { |
|
|
|
/** |
|
|
|
* true if keychain is available and we know that no wallet exists on it |
|
|
|
*/ |
|
|
|
public boolean noWallet() { |
|
|
|
return false; |
|
|
|
public boolean noWallet(Context ctx) { |
|
|
|
String phrase = getKeyStoreString(null, ctx); |
|
|
|
|
|
|
|
return phrase.length() == 0; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
@ -201,8 +389,9 @@ public class BRWalletManager { |
|
|
|
/** |
|
|
|
* true if device passcode is enabled |
|
|
|
*/ |
|
|
|
public boolean isPasscodeEnabled() { |
|
|
|
return false; |
|
|
|
public boolean isPasscodeEnabled(Context ctx) { |
|
|
|
KeyguardManager keyguardManager = (KeyguardManager) ctx.getSystemService(Activity.KEYGUARD_SERVICE); |
|
|
|
return keyguardManager.isKeyguardSecure(); |
|
|
|
} |
|
|
|
|
|
|
|
public void updateFeePerKb() { |
|
|
|