How to securely store the base64EncodedPublicKey key of an Android app?

3

In Android Developer, when we want to integrate the In-app Billing Version 3 library, we find the recommendation that the base64 keys of the application should not be stored flat in the code:

  

/ * base64EncodedPublicKey should be YOUR APPLICATION'S PUBLIC KEY   (that you got from the Google Play developer console). This is not your   developer public key, it's the app-specific public key.

     

Instead of just storing the entire literal string here embedded in the   program, construct the key at runtime from pieces or   use bit manipulation (for com.probarnocuestanada, XOR with some other string) to hide   the current key. The key itself is not secret information, but we do not   want to make it easy for an attacker to replace the public key with one   of their own and then fake messages from the server. * /

String base64EncodedPublicKey = "CONSTRUCT_YOUR_KEY_AND_PLACE_IT_HERE";

According to Android's recommendation, they should be "obfuscated" in some way to be a bit more secure.

Someone could recommend some form on How to securely store the base64EncodedPublicKey key of an Android app?

    
asked by Pablo Ezequiel 04.07.2016 в 18:53
source

3 answers

2

The fingerprint of your application is a unique value that can be obtained programmatically. This value only exists in the developer's own PC (to sign APKs) or at the runtime in the app.

You can use that value as an encryption key; to get it:

public String getCertificateSHA1Fingerprint(Context context) {
    PackageManager pm = context.getPackageManager();
    String packageName = context.getPackageName();
    int flags = PackageManager.GET_SIGNATURES;
    PackageInfo packageInfo = null;
    try {
        packageInfo = pm.getPackageInfo(packageName, flags);
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace();
    }
    Signature[] signatures = packageInfo.signatures;
    byte[] cert = signatures[0].toByteArray();
    InputStream input = new ByteArrayInputStream(cert);
    CertificateFactory cf = null;
    try {
        cf = CertificateFactory.getInstance("X509");
    } catch (CertificateException e) {
        e.printStackTrace();
    }
    X509Certificate c = null;
    try {
        c = (X509Certificate) cf.generateCertificate(input);
    } catch (CertificateException e) {
        e.printStackTrace();
    }
    String hexString = null;
    try {
        MessageDigest md = MessageDigest.getInstance("SHA1");
        byte[] publicKey = md.digest(c.getEncoded());
        hexString = byte2HexFormatted(publicKey);
    } catch (NoSuchAlgorithmException | CertificateEncodingException e1) {
        e1.printStackTrace();
    }
    return hexString;
}

private String byte2HexFormatted(byte[] arr) {
    StringBuilder str = new StringBuilder(arr.length * 2);
    for (int i = 0; i < arr.length; i++) {
        String h = Integer.toHexString(arr[i]);
        int l = h.length();
        if (l == 1) h = "0" + h;
        if (l > 2) h = h.substring(l - 2, l);
        str.append(h.toUpperCase());
        if (i < (arr.length - 1)) str.append(':');
    }
    return str.toString();
}

With the fingerprint of your app:

import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;

public class AES {

    public static SecretKey generateKey(String key) throws NoSuchAlgorithmException {
        MessageDigest digest = MessageDigest.getInstance("SHA-1");
        byte[] passphrase = null;
        try {
            passphrase = digest.digest(key.getBytes("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        passphrase = Arrays.copyOf(passphrase, 16);
        return new SecretKeySpec(passphrase, "AES");
    }

    public static String encrypt(String message, String key) throws Exception {
        byte[] data = message.getBytes("UTF-8");
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, generateKey(key));
        byte[] encryptData = cipher.doFinal(data);

        return byteArrayToHexString(encryptData);
    }

    public static String decrypt(String v, String key) throws Exception {
        byte[] tmp = hexStringToByteArray(v);
        SecretKeySpec spec = new SecretKeySpec(generateKey(key).getEncoded(), "AES");
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, spec);
        String result;
        try {
            result = new String(cipher.doFinal(tmp), StandardCharsets.UTF_8);
        } catch (IllegalBlockSizeException e) {
            result = v;
        }

        return result;

    }

    public static byte[] hexStringToByteArray(String s) {
        int len = s.length();
        byte[] data = new byte[len/2];

        for(int i = 0; i < len; i+=2){
            data[i/2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i+1), 16));
        }

        return data;
    }

    final protected static char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
    public static String byteArrayToHexString(byte[] bytes) {
        char[] hexChars = new char[bytes.length*2];
        int v;

        for(int j=0; j < bytes.length; j++) {
            v = bytes[j] & 0xFF;
            hexChars[j*2] = hexArray[v>>>4];
            hexChars[j*2 + 1] = hexArray[v & 0x0F];
        }

        return new String(hexChars);
    }
}

To save the text, you can do it as a preference:

    public String getSecret() {
        SharedPreferences prefs = getSharedPreferences("com.example.test", Context.MODE_PRIVATE);
        String val = prefs.getString("secret", null);
        return val;
    }

    public void setSecret(String secret) {
        SharedPreferences.Editor editor = getSharedPreferences("com.example.test", Context.MODE_PRIVATE).edit();
        editor.putString("secret", secret).apply();
    }

Summary:

String sha1 = getCertificateSHA1Fingerprint(this); // obtenemos huella digital
String message = "hello_world";
String encrypted = AES.encrypt(message, sha1); // encripta texto

setSecret(encrypted);
String secret = getSecret();
Log.e("TEST", secret);

String decrypted = AES.decrypt(secret, sha1);
Log.e("TEST", decrypted);

This code is used in the AndroidStringObfuscator library, which automates this task.

    
answered by 03.12.2016 / 11:31
source
1

As a rule, no method is foolproof, but there are several alternatives that make it harder to obtain that key.

The best alternative : Have the password on the server. In this way you can not obtain the key by decompiling the app. The counter is that it requires connectivity to work.

Calculate the key : This is what @Pablo Ezequiel mentions using strings and algorithms to assemble the key in real time. Being Base64 could be something as simple as saving the decoded value, ideally separated in different places.

Shared preferences / SQLite : These are the most basic alternatives, the key is not in the code but a person with will can recover these values.

String in the code : This is the worst option since anyone can decompile the APK can find this value exposed, even though the app has been obfuscated using proguard.

    
answered by 27.07.2016 в 01:44
1

Google recommendations in your guide Security and design highlight what's interesting in this case :

  

If possible, you should perform signature verification on a server   remote and not on a device. Implement the verification process   on a server makes it difficult for attackers to interrupt   the process of verification through the application of engineering   Reverse to your .apk file. If you transfer processing   security to a remote server, make sure that the protocol   Link between the device and the server is secure.

Hide your code

  

You must hide your integrated billing code in order to make it   difficult for an attacker to reverse engineer the protocols   of security and other components of the application. At a minimum, you   We recommend running a concealment tool like Proguard in   your code.

Note: If you use Proguard to hide your code, you must add the following line to your Proguard configuration file: -keep class com.android.vending.billing.**

Protect your public key from Google Play

  

To keep your public key protected against malicious users and   hackers, do not incorporate it into any code as a literal string. In   change, build the string at run time from   pieces or use bit manipulation (for example, XOR with some other   string) to hide the real key. The key itself does not represent   secret information, but it will not be convenient for a hacker or user   malicious can easily replace the public key with another.

Searching for SO In app billing security I found how to implement an encryption system in XOR

public static String xorEncrypt(String input, String key) {
    byte[] inputBytes = input.getBytes();
    int inputSize = inputBytes.length;

    byte[] keyBytes = key.getBytes();
    int keySize = keyBytes.length - 1;

    byte[] outBytes = new byte[inputSize];
    for (int i=0; i<inputSize; i++) {
        outBytes[i] = (byte) (inputBytes[i] ^ keyBytes[i % keySize]);
    }

    return new String(Base64.encode(outBytes, Base64.DEFAULT));
}

public static String xorDecrypt(String input, String key) {
    byte[] inputBytes = Base64.decode(input, Base64.DEFAULT);
    int inputSize = inputBytes.length;

    byte[] keyBytes = key.getBytes();
    int keySize = keyBytes.length - 1;

    byte[] outBytes = new byte[inputSize];
    for (int i=0; i<inputSize; i++) {
        outBytes[i] = (byte) (inputBytes[i] ^ keyBytes[i % keySize]);
    }

    return new String(outBytes);
}

I have not tried them yet

    
answered by 02.12.2016 в 13:16