How to secure the api key in android

It’s very common that often we need to use third party paid services like youtube or google map. They provide an api key to integrate their library in the android app building process. If we store these keys locally in our project, publish the app in the playstore and if someone decompiles your apk, your keys will become public. Trust me!! extracting keys from apk is very easy. So how to secure the api key in android? Well, we will learn it today.

We will follow some steps and use free tools to secure our api key. Here is the all steps:

  1. Create anonymous user login via firebase authentication.
  2. Store the key in firebase realtime database or firestore with authenticated read only.
  3. Save that key in encrypted sharedpreference.

Explanation:

First we create the anonymously user login system using firebase authentication, so that user sign up will be done silently in backgroud and new uid will be generated. After that we will store our keys or any string or data in to our firebase realtime database or firestore. You can any of it. But most importantly we will have to make sure that database read is only allowed to authenticated users. And lastly we will download that data from database and store it in our encrypted sharedpreference. Now lets jump into the code. Shall we?

Step 1: Implement Firebase Authentication

Go to your firebase console, Open your project and enable anonymous provider in sign-in-method.

Firebase anonymous authentication

Remember, it’s not necessarily mean that it must have to be anonymous. It can be any provider like google, facebook or phone etc. It’s up to you and your requirement.

Now in your android app you need to install this authentication system. Add these into your gradle:

implementation platform('com.google.firebase:firebase-bom:29.0.0')
implementation 'com.google.firebase:firebase-auth'

Create a FirebaseAuth variable, initialise it in onCreate and add a listener like below:

private FirebaseAuth mAuth;
...
@Override
protected void onCreate(Bundle savedInstanceState) {     
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mAuth = FirebaseAuth.getInstance();

    mAuth.signInAnonymously().addOnCompleteListener(this, new OnCompleteListener() {
        @Override
        public void onComplete(@NonNull Task task) {
            if (task.isSuccessful()) {
                // start read from firebase database
            } else {

            }
        }
    });

}

Authentication is done. If all the things are implemented correctly task.isSuccessful() will return true.

Step 2:Implement Firebase Realtime Database

Go to your firebase console again and store the key in your realtime database like below picture.

Firebase realtime database

Now to secure the database we need to restrict the read/write access. Click on Rules and paste the below code:

{
  "rules": {
    ".read": "auth != null",
    ".write": "false"
  }
}
Securing firebase database

This means no one but only the user who are from your app will be able to read the database.

Add the below line in gradle.

implementation 'com.google.firebase:firebase-database'

Now create database variable and initialize it like before.

private DatabaseReference mDatabase;
...
@Override
protected void onCreate(Bundle savedInstanceState) {     
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mAuth = FirebaseAuth.getInstance();
    mDatabase = FirebaseDatabase.getInstance().getReference("key");

    mAuth.signInAnonymously().addOnCompleteListener(this, new OnCompleteListener() {
        @Override
        public void onComplete(@NonNull Task task) {
            if (task.isSuccessful()) {
                // start read from firebase database
                storeKeyInSharedPreference(snapshot.getValue().toString());
                readFromSharedPreference();
            } else {

            }
        }
    });

}

Step 3: Implement Encrypted Sharedpreference

We are almost done. It’s time to create the encrypted sharedpreference now. Add the below code in gradle again.

implementation "androidx.security:security-crypto:1.1.0-alpha01"

Create and object of SharedPreferences and implement the missing methods which we declared just right now.


private SharedPreferences sharedPreferences;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ....
    ....
    ....
}
private void storeKeyInSharedPreference(String key) {
    String sharedPrefsFile = "myKeys";
    try {
        sharedPreferences = EncryptedSharedPreferences.create(
                this,
                sharedPrefsFile,
                getMasterKey(),
                EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
                EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
        );

        SharedPreferences.Editor sharedPrefsEditor = sharedPreferences.edit();
        sharedPrefsEditor.putString("key", key);
        sharedPrefsEditor.commit();
    } catch (GeneralSecurityException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}


private void readFromSharedPreference() {   
    Log.e("after store", sharedPreferences.getString("key", "error")
}

private MasterKey getMasterKey() {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
                MasterKey.DEFAULT_MASTER_KEY_ALIAS,
                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                .setKeySize(256)
                .build()
        try {
            return new MasterKey.Builder(this)
                    .setKeyGenParameterSpec(spec)
                    .build();
        } catch (Exception e) {
            Log.e("M", e.getMessage());
        }
    } else {
        try {
            return new MasterKey.Builder(this)
                    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build();
        } catch (Exception e) {
        }
    }

    return null;
}

Notice that getMasterKey() is compatible with Android Lollipop. Now whatever you save in your sharedpreference it will be encrypted automatically. It will be very difficult to extract the data from encrypted sharedpreference.

The whole code is as follows:

package com.captaindroid.secureapi;


import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.security.crypto.EncryptedSharedPreferences;
import androidx.security.crypto.MasterKey;
import androidx.security.crypto.MasterKeys;

import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;

import java.io.IOException;
import java.security.GeneralSecurityException;

public class MainActivity extends AppCompatActivity {
    private String TAG = "Main";

    private FirebaseAuth mAuth;

    private DatabaseReference mDatabase;
    private SharedPreferences sharedPreferences;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mAuth = FirebaseAuth.getInstance();
        mDatabase = FirebaseDatabase.getInstance().getReference("key");

        mAuth.signInAnonymously().addOnCompleteListener(this, new OnCompleteListener() {
            @Override
            public void onComplete(@NonNull Task task) {
                if (task.isSuccessful()) {
                    mDatabase.addValueEventListener(new ValueEventListener() {
                        @Override
                        public void onDataChange(@NonNull DataSnapshot snapshot) {
                            Log.e("key", snapshot.getValue().toString());
                            storeKeyInSharedPreference(snapshot.getValue().toString());
                            readFromSharedPreference();
                        }

                        @Override
                        public void onCancelled(@NonNull DatabaseError error) {
                        }

                    });
                } else {

                }
            }
        });

    }

    private void storeKeyInSharedPreference(String key) {
        String sharedPrefsFile = "myKeys";
        try {
             sharedPreferences = EncryptedSharedPreferences.create(
                    this,
                    sharedPrefsFile,
                    getMasterKey(),
                    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
                    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
            );

            SharedPreferences.Editor sharedPrefsEditor = sharedPreferences.edit();
            sharedPrefsEditor.putString("key", key);
            sharedPrefsEditor.commit();
        } catch (GeneralSecurityException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    private void readFromSharedPreference() {
        Log.e("after store", sharedPreferences.getString("key", "error"));
    }

    private MasterKey getMasterKey() {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
                    MasterKey.DEFAULT_MASTER_KEY_ALIAS,
                    KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                    .setKeySize(256)
                    .build();

            try {
                return new MasterKey.Builder(this)
                        .setKeyGenParameterSpec(spec)
                        .build();
            } catch (Exception e) {
                Log.e("M", e.getMessage());
            }

        } else {
            try {
                return new MasterKey.Builder(this)
                        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build();
            } catch (Exception e) {
            }
        }

        return null;
    }

}

Conclusion:

Remember, nothing is 100% secure. We can not guaranty anything unbreakable. What we can do is to make it strong and difficult enough to be hacked.