Daemon RPC authentication with encrypted api key storage #10
2 changed files with 206 additions and 1 deletions
|
@ -1,9 +1,49 @@
|
||||||
package io.lbry.lbrynet;
|
package io.lbry.lbrynet;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.security.KeyPairGeneratorSpec;
|
||||||
|
import android.util.Base64;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.Key;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.NoSuchProviderException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.CipherInputStream;
|
||||||
|
import javax.crypto.CipherOutputStream;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import javax.security.auth.x500.X500Principal;
|
||||||
|
|
||||||
public final class Utils {
|
public final class Utils {
|
||||||
|
|
||||||
|
private static final String TAG = Utils.class.getName();
|
||||||
|
|
||||||
|
private static final String AES_MODE = "AES/ECB/PKCS7Padding";
|
||||||
|
|
||||||
|
private static final String KEY_ALIAS = "LBRYKey";
|
||||||
|
|
||||||
|
private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
|
||||||
|
|
||||||
|
private static final String RSA_MODE = "RSA/ECB/PKCS1Padding";
|
||||||
|
|
||||||
|
private static final String SP_NAME = "app";
|
||||||
|
|
||||||
|
private static final String SP_ENCRYPTION_KEY = "key";
|
||||||
|
|
||||||
|
private static final String SP_API_SECRET_KEY = "api_secret";
|
||||||
|
|
||||||
public static String getAndroidRelease() {
|
public static String getAndroidRelease() {
|
||||||
return android.os.Build.VERSION.RELEASE;
|
return android.os.Build.VERSION.RELEASE;
|
||||||
}
|
}
|
||||||
|
@ -55,4 +95,131 @@ public final class Utils {
|
||||||
|
|
||||||
return file.getAbsolutePath();
|
return file.getAbsolutePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void saveApiSecret(String secret, Context context, KeyStore keyStore) {
|
||||||
|
try {
|
||||||
|
SharedPreferences pref = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||||
|
SharedPreferences.Editor editor = pref.edit();
|
||||||
|
editor.putString(SP_API_SECRET_KEY, encrypt(secret.getBytes(), context, keyStore));
|
||||||
|
editor.commit();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Log.e(TAG, "lbrynetservice - Could not save the API secret", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String loadApiSecret(Context context, KeyStore keyStore) {
|
||||||
|
SharedPreferences pref = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||||
|
String encryptedSecret = pref.getString(SP_API_SECRET_KEY, null);
|
||||||
|
if (encryptedSecret != null && encryptedSecret.trim().length() > 0) {
|
||||||
|
try {
|
||||||
|
byte[] decoded = Base64.decode(encryptedSecret, Base64.DEFAULT);
|
||||||
|
return new String(decrypt(decoded, context, keyStore), Charset.forName("UTF8"));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Log.e(TAG, "lbrynetservice - Could not load the API secret", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String encrypt(byte[] input, Context context, KeyStore keyStore) throws Exception {
|
||||||
|
Cipher c = Cipher.getInstance(AES_MODE, "BC");
|
||||||
|
c.init(Cipher.ENCRYPT_MODE, getSecretKey(context, keyStore));
|
||||||
|
return Base64.encodeToString(c.doFinal(input), Base64.DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] decrypt(byte[] encrypted, Context context, KeyStore keyStore) throws Exception {
|
||||||
|
Cipher c = Cipher.getInstance(AES_MODE, "BC");
|
||||||
|
c.init(Cipher.DECRYPT_MODE, getSecretKey(context, keyStore));
|
||||||
|
return c.doFinal(encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final KeyStore initKeyStore(Context context) throws Exception {
|
||||||
|
KeyStore ks = KeyStore.getInstance(KEYSTORE_PROVIDER);
|
||||||
|
ks.load(null);
|
||||||
|
|
||||||
|
if (!ks.containsAlias(KEY_ALIAS)) {
|
||||||
|
Calendar start = Calendar.getInstance();
|
||||||
|
Calendar end = Calendar.getInstance();
|
||||||
|
end.add(Calendar.YEAR, 100);
|
||||||
|
|
||||||
|
KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context)
|
||||||
|
.setAlias(KEY_ALIAS)
|
||||||
|
.setSubject(new X500Principal(String.format("CN=%s", KEY_ALIAS)))
|
||||||
|
.setSerialNumber(BigInteger.ONE)
|
||||||
|
.setStartDate(start.getTime())
|
||||||
|
.setEndDate(end.getTime())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try {
|
||||||
|
KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA", KEYSTORE_PROVIDER);
|
||||||
|
keygen.initialize(spec);
|
||||||
|
keygen.generateKeyPair();
|
||||||
|
} catch (NoSuchProviderException ex) {
|
||||||
|
throw ex;
|
||||||
|
} catch (InvalidAlgorithmParameterException ex) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] rsaEncrypt(byte[] secret, KeyStore keyStore) throws Exception {
|
||||||
|
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(KEY_ALIAS, null);
|
||||||
|
|
||||||
|
// Encrypt the text
|
||||||
|
Cipher inputCipher = Cipher.getInstance(RSA_MODE);
|
||||||
|
inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.getCertificate().getPublicKey());
|
||||||
|
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, inputCipher);
|
||||||
|
cipherOutputStream.write(secret);
|
||||||
|
cipherOutputStream.close();
|
||||||
|
|
||||||
|
return outputStream.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] rsaDecrypt(byte[] encrypted, KeyStore keyStore) throws Exception {
|
||||||
|
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(KEY_ALIAS, null);
|
||||||
|
Cipher output = Cipher.getInstance(RSA_MODE);
|
||||||
|
output.init(Cipher.DECRYPT_MODE, privateKeyEntry.getPrivateKey());
|
||||||
|
CipherInputStream cipherInputStream = new CipherInputStream(new ByteArrayInputStream(encrypted), output);
|
||||||
|
ArrayList<Byte> values = new ArrayList<Byte>();
|
||||||
|
int nextByte;
|
||||||
|
while ((nextByte = cipherInputStream.read()) != -1) {
|
||||||
|
values.add((byte) nextByte);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bytes = new byte[values.size()];
|
||||||
|
for(int i = 0; i < bytes.length; i++) {
|
||||||
|
bytes[i] = values.get(i).byteValue();
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String generateSecretKey(Context context, KeyStore keyStore) throws Exception {
|
||||||
|
byte[] key = new byte[16];
|
||||||
|
SecureRandom secureRandom = new SecureRandom();
|
||||||
|
secureRandom.nextBytes(key);
|
||||||
|
|
||||||
|
byte[] encryptedKey = rsaEncrypt(key, keyStore);
|
||||||
|
String base64Encrypted = Base64.encodeToString(encryptedKey, Base64.DEFAULT);
|
||||||
|
|
||||||
|
SharedPreferences pref = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||||
|
SharedPreferences.Editor editor = pref.edit();
|
||||||
|
editor.putString(SP_ENCRYPTION_KEY, base64Encrypted);
|
||||||
|
editor.commit();
|
||||||
|
|
||||||
|
return base64Encrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Key getSecretKey(Context context, KeyStore keyStore) throws Exception{
|
||||||
|
SharedPreferences pref = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||||
|
String base64Key = pref.getString(SP_ENCRYPTION_KEY, null);
|
||||||
|
if (base64Key == null || base64Key.trim().length() == 0) {
|
||||||
|
base64Key = generateSecretKey(context, keyStore);
|
||||||
|
}
|
||||||
|
return new SecretKeySpec(rsaDecrypt(Base64.decode(base64Key, Base64.DEFAULT), keyStore), "AES");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,44 @@ lbrynet.androidhelpers.paths.android_external_storage_dir = lambda: lbrynet_util
|
||||||
lbrynet.androidhelpers.paths.android_app_internal_storage_dir = lambda: lbrynet_utils.getAppInternalStorageDir(service.getApplicationContext())
|
lbrynet.androidhelpers.paths.android_app_internal_storage_dir = lambda: lbrynet_utils.getAppInternalStorageDir(service.getApplicationContext())
|
||||||
lbrynet.androidhelpers.paths.android_app_external_storage_dir = lambda: lbrynet_utils.getAppExternalStorageDir(service.getApplicationContext())
|
lbrynet.androidhelpers.paths.android_app_external_storage_dir = lambda: lbrynet_utils.getAppExternalStorageDir(service.getApplicationContext())
|
||||||
|
|
||||||
|
# RPC authentication secret
|
||||||
|
# Retrieve the Anroid keystore
|
||||||
|
ks = lbrynet_utils.initKeyStore(service.getApplicationContext());
|
||||||
|
|
||||||
|
import lbrynet.daemon.auth
|
||||||
|
from lbrynet.daemon.auth.util import APIKey, API_KEY_NAME
|
||||||
|
|
||||||
|
def load_api_keys(path):
|
||||||
|
key_name = API_KEY_NAME
|
||||||
|
context = service.getApplicationContext();
|
||||||
|
secret = lbrynet_utils.loadApiSecret(context, ks)
|
||||||
|
# TODO: For testing. Normally, this should not be displayed.
|
||||||
|
log.info('Loaded API Secret: %s', secret);
|
||||||
|
return { key_name: APIKey(secret, key_name, None) }
|
||||||
|
|
||||||
|
def save_api_keys(keys, path):
|
||||||
|
key_name = API_KEY_NAME
|
||||||
|
if key_name in keys:
|
||||||
|
secret = keys[key_name].secret
|
||||||
|
# TODO: For testing. Normally, this should not be displayed.
|
||||||
|
log.info('Saving API Secret: %s', secret);
|
||||||
|
context = service.getApplicationContext();
|
||||||
|
lbrynet_utils.saveApiSecret(secret, context, ks)
|
||||||
|
|
||||||
|
def initialize_api_key_file(key_path):
|
||||||
|
context = service.getApplicationContext();
|
||||||
|
secret = lbrynet_utils.loadApiSecret(context, ks)
|
||||||
|
if secret is None:
|
||||||
|
keys = {}
|
||||||
|
new_api_key = APIKey.new(name=API_KEY_NAME)
|
||||||
|
keys.update({new_api_key.name: new_api_key})
|
||||||
|
save_api_keys(keys, key_path)
|
||||||
|
|
||||||
|
|
||||||
|
lbrynet.daemon.auth.util.load_api_keys = load_api_keys
|
||||||
|
lbrynet.daemon.auth.util.save_api_keys = save_api_keys
|
||||||
|
lbrynet.daemon.auth.util.initialize_api_key_file = initialize_api_key_file
|
||||||
|
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
|
|
||||||
from lbrynet.core import log_support
|
from lbrynet.core import log_support
|
||||||
|
@ -72,7 +110,7 @@ def start():
|
||||||
|
|
||||||
if test_internet_connection():
|
if test_internet_connection():
|
||||||
analytics_manager = analytics.Manager.new_instance()
|
analytics_manager = analytics.Manager.new_instance()
|
||||||
start_server_and_listen(False, analytics_manager)
|
start_server_and_listen(True, analytics_manager)
|
||||||
reactor.run()
|
reactor.run()
|
||||||
else:
|
else:
|
||||||
log.info("Not connected to internet, unable to start")
|
log.info("Not connected to internet, unable to start")
|
||||||
|
|
Loading…
Reference in a new issue