Android Keystore getEntry() and generateKeyPair()

2019-03-09 11:20发布

问题:

My Android app need to encrypt a file so that it can decrypt and read it later. This should not be decrypt-able by anybody else other than the app, even user.

Following is how I am doing the encryption and decryption. This works most of the time, but some times for some users this is failing. It is not specific to a particular handset (Nexus7, Samsung, Motorola, HTC -- all types are reporting this issue), but not all users are experiencing it. Only some users occasionally.

Here is the relevant code:

encrypt() {
   KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
   final KeyStore.PrivateKeyEntry entry;
   if (!ks.containsAlias(CERT_ALIAS)) {
       Calendar cal = Calendar.getInstance();
       Date now = cal.getTime();
       cal.add(Calendar.YEAR, 50);
       Date end = cal.getTime();
       KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
       kpg.initialize(new KeyPairGeneratorSpec.Builder(getApplicationContext())
              .setAlias(CERT_ALIAS)
              .setStartDate(now)
              .setEndDate(end)
              .setSerialNumber(BigInteger.valueOf(1))
              .setSubject(new X500Principal("CN=" + CERT_ALIAS))
              .build());
       KeyPair kp = kpg.generateKeyPair();
   }
   entry = (KeyStore.PrivateKeyEntry) ks.getEntry(
                     CERT_ALIAS, null);
   pub = entry.getCertificate().getPublicKey();
   // use the pub key to encrypt
}
decrypt() {
    KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
    ks.load(null);

    final KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) ks.getEntry(
            CERT_ALIAS, null);
    PrivateKey key1 = entry.getPrivateKey();
    // use the private key decrypt
}

This code sometimes throws

java.lang.RuntimeException: error:0D07207B:asn1 encoding routines:ASN1_get_object:header too long
at com.android.org.conscrypt.NativeCrypto.ENGINE_load_private_key(Native Method)
at com.android.org.conscrypt.OpenSSLEngine.getPrivateKeyById(OpenSSLEngine.java:66)
at android.security.AndroidKeyStore.engineGetKey(AndroidKeyStore.java:86)
at java.security.KeyStoreSpi.engineGetEntry(KeyStoreSpi.java:372)
at java.security.KeyStore.getEntry(KeyStore.java:644)

So I modified encrypt() to first try to get the entry and if it raises exception, generate new key pair.

final KeyStore.PrivateKeyEntry entry = null;
if (ks.containsAlias(CERT_ALIAS)) {
    try {
        entry = (KeyStore.PrivateKeyEntry) ks.getEntry(
                      CERT_ALIAS, null);
    } catch (Exception e) {
    }
}
if (entry == null) {
    //generate new key pair
}

But even this is failing sometimes with the following exception.

java.lang.IllegalStateException: could not generate key in keystore
at android.security.AndroidKeyPairGenerator.generateKeyPair(AndroidKeyPairGenerator.java:100)
at java.security.KeyPairGenerator$KeyPairGeneratorImpl.generateKeyPair(KeyPairGenerator.java:275)
  1. What am I doing wrong?
  2. How do I fix it/work around it?
  3. Does these exceptions indicate that the files are being tampered with?
  4. Does this happen for users with screenlock password/pin?
  5. Before I generate new pair, should I delete the entry? (KeyStore.deleteEntry())

I observed that the keystore returns null after screenlock password/pin change. Some others also seem to have experienced this issue (KeyStore getEntry return null after change password)

回答1:

I ran into the could not generate key in keystore issue with one of my apps and after digging deep into it with one of the affected phones I discovered that some devices have it set up that the phone's unlock pattern/pass/pin is different than the password that actually unlocks the key storage. If you want to double check that that is your issue as well you can use the work here: http://nelenkov.blogspot.com/2012/05/storing-application-secrets-in-androids.html to get at the actual private system api the public KeyPairGenerator objects are calling and check the return code on it. I'm not sure why Google decided to hide the return code behind a boolean but there you have it.

You can trigger an unlock of the keystore manually by calling startActivity(new Intent("com.android.credentials.UNLOCK")); but this might not really help much. From what I've seen if a phone is in this state it is because some Device Administrator app locked the keystore in the background so it can set up VPN or email credentials. This means the user doesn't actually know the password. I'm still looking into a workaround (possibly find out how the Device Administrator apps get access to the keystore so I can unlock it that way) but it's a hairy issue to say the least. I'll try to update this if I find out more in my explorations, hopefully this at least points some people in the right direction.