Any way around awful SecretKeyFactory performance

2019-03-14 01:30发布

I'm looking to use the new licensing (LVL) stuff with Android Marketplace, but I'm running into a performance problem with the stock AESObfuscator. Specifically, the constructor takes several seconds to run on a device (pure agony on emulator). Since this code needs to run to even check for cached license responses, it puts a serious damper on checking the license at startup.

Running the LVL sample app, here's my barbarian-style profiling of AESObfuscator's constructor:

public AESObfuscator(byte[] salt, String applicationId, String deviceId) {
        Log.w("AESObfuscator", "constructor starting");
        try {
            Log.w("AESObfuscator", "1");
            SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM);
            Log.w("AESObfuscator", "2");
            KeySpec keySpec =
                new PBEKeySpec((applicationId + deviceId).toCharArray(), salt, 1024, 256);
            Log.w("AESObfuscator", "3");
            SecretKey tmp = factory.generateSecret(keySpec);
            Log.w("AESObfuscator", "4");
            SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
            Log.w("AESObfuscator", "5");
            mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM);
            Log.w("AESObfuscator", "6");
            mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV));
            Log.w("AESObfuscator", "7");
            mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM);
            Log.w("AESObfuscator", "8");
            mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV));
        } catch (GeneralSecurityException e) {
            // This can't happen on a compatible Android device.
            throw new RuntimeException("Invalid environment", e);
        }
        Log.w("AESObfuscator", "constructor done");
    }

The output on a Nexus One:

09-28 09:29:02.799: INFO/System.out(12377): debugger has settled (1396)
09-28 09:29:02.988: WARN/AESObfuscator(12377): constructor starting
09-28 09:29:02.988: WARN/AESObfuscator(12377): 1
09-28 09:29:02.999: WARN/AESObfuscator(12377): 2
09-28 09:29:02.999: WARN/AESObfuscator(12377): 3
09-28 09:29:09.369: WARN/AESObfuscator(12377): 4
09-28 09:29:09.369: WARN/AESObfuscator(12377): 5
09-28 09:29:10.389: WARN/AESObfuscator(12377): 6
09-28 09:29:10.398: WARN/AESObfuscator(12377): 7
09-28 09:29:10.398: WARN/AESObfuscator(12377): 8
09-28 09:29:10.409: WARN/AESObfuscator(12377): constructor done
09-28 09:29:10.409: WARN/ActivityManager(83): Launch timeout has expired, giving up wake lock!
09-28 09:29:10.458: INFO/LicenseChecker(12377): Binding to licensing service.

7 seconds of thrashing (about 20 in emulator, ugh). I can spin it off on an AsyncTask, but it doesn't do much good there, since the app can't really run until I've validated the license. All I get is a nice, pretty seven seconds of progress bar while the user waits for me to check the license.

Any ideas? Roll my own obfuscator with something simpler than AES to cache my own license responses?

5条回答
看我几分像从前
2楼-- · 2019-03-14 02:00

I've also optimized it, but kept it all in one class. I've made the Cipher's static so they only have to created once and then changed the keygen algorithm it 128bit with MD5 instead of SHA1. LicenseCheckerCallback now fires within a half a second instead of the 3 second wait before.

public class AESObfuscator implements Obfuscator {

private static final String KEYGEN_ALGORITHM = "PBEWithMD5And128BitAES-CBC-OpenSSL";
// Omitted all other the other unchanged variables

private static Cipher mEncryptor = null;
private static Cipher mDecryptor = null;

public AESObfuscator(byte[] salt, String applicationId, String deviceId) {

    if (null == mEncryptor || null == mDecryptor) {
        try {
            // Anything in here was unchanged
        } catch (GeneralSecurityException e) {
            // This can't happen on a compatible Android device.
            throw new RuntimeException("Invalid environment", e);
        }
    }
}
查看更多
女痞
3楼-- · 2019-03-14 02:12

After extensive searching and tinkering, my best workaround seems to be to create the AES key on my own, rather than using the PKCS#5 code in PBEKeySpec. I am somewhat amazed that other people have not posted this problem.

The workaround method is to combine a bunch of identifying data (device id, IMEI, package name, etc) into a string. I then take the SHA-1 hash of that string to get 20 bytes of the 24-byte AES key. Admittedly, there's not as much entropy as PKCS#5 and 4 bytes of the key are known. But, really, who is going to mount a crypto attack? It's still pretty sound and there are much weaker attack points in the LVL, despite my other attempts at hardening it.

Since even creating the AES cipher seems to be an expensive (2 secs on emulator) operation, I also defer creation of the encryptor and decryptor members until they are needed by calls to obfuscate and deobfuscate. When the app is using a cached license response, it does not need an encryptor; this cuts quite a bit of cycle out of the most common startup mode.

My new constructor is below. If anyone wants the whole source file, just drop me a line.

   /**
    * @param initialNoise device/app identifier. Use as many sources as possible to
    *    create this unique identifier.
    */
   public PixieObfuscator(String initialNoise) {
        try {
            // Hash up the initial noise into something smaller:
            MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM);
            md.update(initialNoise.getBytes());
            byte[] hash = md.digest();

            // Allocate a buffer for our actual AES key:
            byte[] aesKEY = new byte[AES_KEY_LENGTH];   

            // Fill it with our lucky byte to take up whatever slack is not filled by hash:
            Arrays.fill(aesKEY, LUCKY_BYTE);

            // Copy in as much of the hash as we got (should be twenty bytes, take as much as we can):
            for (int i = 0; i < hash.length && i < aesKEY.length; i++)
                aesKEY[i] = hash[i];

            // Now make the damn AES key object:
              secret = new SecretKeySpec(aesKEY, "AES");
        }
        catch (GeneralSecurityException ex) {
            throw new RuntimeException("Exception in PixieObfuscator constructor, invalid environment");
        }
   }
查看更多
不美不萌又怎样
4楼-- · 2019-03-14 02:13

Rather than re-writing the obfuscator, it makes more sense to run it in another thread. Sure, crackers can use your app in the meantime, but so what? 3 seconds is not enough time for them to do anything useful, but it is a looong time for legitimate users to wait for license approval.

查看更多
ゆ 、 Hurt°
5楼-- · 2019-03-14 02:14

I ran into to the same issue.

What I did was to put the license initialization into a asynctask with the lowest thread priority that is possible by using:

android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);

in the method

doInBackground

But when showing the license not valid dialog this will then be done in the GUI thread.

So my license check looks like:

public class LicenseHandler {

    private LicenseHandlerTask task;

public LicenseHandler(final Activity context) {
    super();
    task = new LicenseHandlerTask(context);
    task.execute();
}
/**
 * This will run the task with the lowest thread priority because the
 * AESObfuscator is very slow and will have effect on the performance of the
 * app.<br>
 * 
 */
private static class LicenseHandlerDelay extends
        AsyncTask<Void, Void, ImplLicenseHandler> {
    private final Activity context;

    public LicenseHandlerDelay(final Activity context) {
        this.context = context;
    }

    @Override
    protected ImplLicenseHandler doInBackground(final Void... params) {
        // set the lowest priority available for this task
          android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);

        ImplLicenseHandler checker = new ImplLicenseHandler(context);
        return checker;
    }

    @Override
    protected void onPostExecute(final ImplLicenseHandler result) {
                    checker.check();
    }

}

/**
 * cancels the background task for checking the license if it is running
 */
public void destroy() {
    try {
        if (null != task) {
            task.cancel(true);
            task = null;
        }
    } catch (Throwable e) {
        // regardless of errors
    }
}
}

The LicenseHandler implementation looks like

public class ImplLicenseHandler {

    ...

    private Context mContext = null;
    private AndroidPitLicenseChecker mChecker = null;
    private LicenseCheckerCallback mLicenseCheckerCallback = null;

    public ImplLicencseHandler(Context context){
            this.mContext = context;
            final ServerManagedPolicy googleLicensePolicy = new LicensePolicy(
            mContext, new AESObfuscator(ImplLicenseHandler.SALT,mContext.getPackageName(), ImplLicenseHandler.DEVICE_ID));
            mChecker = new AndroidPitLicenseChecker(mContext,
            mContext.getPackageName(),
            ImplLicenseHandler.ANDROIDPIT_PUBLIC_KEY, googleLicensePolicy,
            ImplLicenseHandler.GOOGLE_LICENSE_PUBLIC_KEY);
            mLicenseCheckerCallback = new LicenseCheckerCallback();
     }

     public void check(){
            mContext.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                         mChecker.checkAccess(mLicenseCheckerCallback);
                    }
            });
     }

     ...

}

But remember: if your LicenseCheckerCallback does show any GUI element, then you have to execute that methods by using

context.runOnUIThread(action);
查看更多
Juvenile、少年°
6楼-- · 2019-03-14 02:24

Ok this works

public class AESObfuscator implements Obfuscator {

private static final String KEYGEN_ALGORITHM = "PBEWithMD5And128BitAES-CBC-OpenSSL";
// Omitted all other the other unchanged variables

private static Cipher mEncryptor = null;
private static Cipher mDecryptor = null;

public AESObfuscator(byte[] salt, String applicationId, String deviceId) {

    if (null == mEncryptor || null == mDecryptor) {
        try {
            // Anything in here was unchanged
        } catch (GeneralSecurityException e) {
            // This can't happen on a compatible Android device.
            throw new RuntimeException("Invalid environment", e);
        }
    }
}
查看更多
登录 后发表回答