Merge pull request #10 from lbryio/rpc_auth
Daemon RPC authentication with encrypted api key storage
This commit is contained in:
commit
da1ce1cc4e
2 changed files with 206 additions and 1 deletions
|
@ -1,9 +1,49 @@
|
|||
package io.lbry.lbrynet;
|
||||
|
||||
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.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 {
|
||||
|
||||
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() {
|
||||
return android.os.Build.VERSION.RELEASE;
|
||||
}
|
||||
|
@ -55,4 +95,131 @@ public final class Utils {
|
|||
|
||||
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_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
|
||||
|
||||
from lbrynet.core import log_support
|
||||
|
@ -72,7 +110,7 @@ def start():
|
|||
|
||||
if test_internet_connection():
|
||||
analytics_manager = analytics.Manager.new_instance()
|
||||
start_server_and_listen(False, analytics_manager)
|
||||
start_server_and_listen(True, analytics_manager)
|
||||
reactor.run()
|
||||
else:
|
||||
log.info("Not connected to internet, unable to start")
|
||||
|
|
Loading…
Reference in a new issue