Android上の鍵、証明書、ストレージ


通常、サードパーティのサービスを使用する場合は、何らかの形式の認証が必要です.これは、ユーザー名とパスワードを受け入れる/loginエンドポイントと同じように簡単かもしれません.
一見、簡単な解決策は、UIを構築し、ユーザーにログインを要求し、ログイン証明書をキャプチャして保存することです.ただし、アプリケーションがサードパーティのアカウントの証明書を知る必要がないため、これは最善の方法ではありません.代わりに、お客様マネージャを使用して、機密情報の処理を依頼することができます.
顧客マネージャ
アカウントマネージャは、ユーザーアカウント証明書の集中的なヘルプであるため、アプリケーションがパスワードを直接処理する必要はありません.通常、実際のユーザー名とパスワードの代わりにトークンを提供し、サービスに認証された要求を発行するために使用できます.一例は、OAuth 2トークンを要求する場合である.
必要なすべての情報がデバイスに格納されている場合があり、アカウントマネージャがリフレッシュのトークンを取得するためにサーバを呼び出す必要がある場合があります.デバイスの「設定」で、さまざまなアプリケーションの「アカウント」セクションが表示されている可能性があります.使用可能な勘定科目のリストを取得できます.
AccountManager accountManager = AccountManager.get(this);
Account[] accounts = accountManager.getAccounts();

このコードにはandroid.permission.GET_ACCOUNTSの権限が必要です.特定の勘定科目を探している場合は、次のようにします.
AccountManager accountManager = AccountManager.get(this);
Account[] accounts = accountManager.getAccountsByType("com.google");

アカウントを所有すると、getAuthToken(Account, String, Bundle, Activity, AccountManagerCallback, Handler)メソッドを呼び出すことで、そのアカウントのトークンを取得できます.その後、トークンを使用して、サービスに対して認証されたAPI要求を発行することができる.これはRESTful APIかもしれません.ユーザーのプライベートアカウントの詳細を知らなくても、HTTPSリクエスト中にトークンパラメータを渡すことができます.
各サービスには、異なる認証方法と専用証明書の格納方法があるため、アカウントマネージャはサードパーティのサービス実装のための認証モジュールを提供します.Androidは多くのポピュラーなサービスを実装しています.これは、アプリケーションのアカウント認証と認証ストレージを処理するために独自の認証器を作成できることを意味します.これにより、認証情報が暗号化されていることを確認できます.これは、他のサービスで使用されているアカウントマネージャの認証情報が明示的に格納されていることを意味し、植根デバイスの誰にでも表示されることを意味します.
簡単な証明書ではなく、個人またはエンティティの鍵または証明書を処理する必要がある場合があります.たとえば、3番目の方向に保持する証明書ファイルを送信する場合などです.最も一般的なのは、アプリケーションがプライベート組織のサーバに認証する必要がある場合です.
次のチュートリアルでは、証明書を使用して認証と安全な通信を検討しますが、これらのプロジェクトを同時にどのように保存するかを解決したいと思っています.鍵列APIは、最初は特定の用途のために構築されたものであり、PKCS#12ファイルから秘密鍵または証明書ペアをインストールする.
キーホルダー
Android 4.0で(APIレベル14)に導入されたKeychain API処理鍵管理.具体的には、PrivateKeyおよびX509Certificateオブジェクトとともに使用され、アプリケーションを使用するデータストアよりも安全なコンテナが提供されます.これは、秘密鍵に対する権限は、自分のアプリケーションが秘密鍵にアクセスすることを許可し、ユーザーが許可した後にのみ秘密鍵にアクセスできるためです.これは、必ず認証情報ストレージを使用するには、デバイスにロック画面を設定する必要があります.同様に、キー列のオブジェクトは、セキュリティハードウェア(ある場合)にバインドできます.
証明書をインストールするコードは次のとおりです.
Intent intent = KeyChain.createInstallIntent();
byte[] p12Bytes = //... read from file, such as example.pfx or example.p12...
intent.putExtra(KeyChain.EXTRA_PKCS12, p12Bytes);
startActivity(intent);

秘密鍵にアクセスするためにパスワードを入力するよう求められ、証明書に名前を付けるよう求められます.鍵を取得するには、以下のコードにUIがあり、ユーザーがインストールされた鍵のリストから選択できるようにします.
KeyChain.choosePrivateKeyAlias(this, this, new String[]{"RSA"}, null, null, -1, null);

選択すると、alias(final String alias)コールバックでalias(final String alias)が返され、秘密鍵または証明書チェーンに直接アクセスできます.
public class KeychainTest extends Activity implements ..., KeyChainAliasCallback
{
    //...
    
    @Override
    public void alias(final String alias)
    {
        Log.e("MyApp", "Alias is " + alias);

        try
        {
            PrivateKey privateKey = KeyChain.getPrivateKey(this, alias);
            X509Certificate[] certificateChain = KeyChain.getCertificateChain(this, alias);
        }
        catch ...
    }
    
    //...
}

これらの知識があれば、認証ストレージを使用して機密データを保存する方法を見てみましょう.
キーストア
前のチュートリアルでは、ユーザーが提供するパスワードによってデータを保護することを検討しました.この設定は良いですが、アプリケーションは通常、ユーザーがログインするたびに他のパスワードを覚えないようにする必要があります.
これがKeyStore APIが使える場所です.API 1から、システムはWiFiおよびVPN認証情報を格納するためにKeyStoreを使用している.4.3(API 18)から開始すると、独自のアプリケーション固有の非対称鍵を使用することができ、Android M(API 23)にAES対称鍵を格納することができる.したがって、APIは機密文字列を直接格納することは許されないが、これらの鍵を格納して文字列を暗号化することができる.
鍵をKeyStoreに格納する利点は、鍵の秘密内容を暴露することなく鍵を操作できることである.重要なデータはアプリケーションスペースに入りません.鍵は権限によって保護されているため、アプリケーションのみがアクセスでき、デバイスが機能している場合は、安全なハードウェアサポートがある可能性があります.これにより、デバイスから鍵を抽出するのがさらに困難になるコンテナが作成されます.
新しいランダムキーを生成
この例では、ユーザーが提供したパスワードからAES鍵を生成するのではなく、KeyStoreで保護されるランダム鍵を自動的に生成することができます.このため、KeyGeneratorのインスタンスを作成し、"AndroidKeyStore"プロバイダに設定することができます.
//Generate a key and store it in the KeyStore
final KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
final KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder("MyKeyAlias",
        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
        //.setUserAuthenticationRequired(true) //requires lock screen, invalidated if lock screen is disabled
        //.setUserAuthenticationValidityDurationSeconds(120) //only available x seconds from password authentication. -1 requires finger print - every time
        .setRandomizedEncryptionRequired(true) //different ciphertext for same plaintext on each call
        .build();
keyGenerator.init(keyGenParameterSpec);
keyGenerator.generateKey();

ここで注意すべき重要な部分は、.setUserAuthenticationRequired(true)および.setUserAuthenticationValidityDurationSeconds(120)仕様である.これらの要件は、ユーザーが認証を通過するまで、スクリーンをロックし、キーをロックすることです..setUserAuthenticationValidityDurationSeconds()のドキュメントを参照すると、鍵はパスワード認証後の一定秒以内にのみ使用可能であり、-1に送信されるたびに指紋認証が必要であることがわかります.認証を有効にするには、ユーザーがロック画面を削除または変更したときに鍵を取り消す効果もあります.
これらのオプションは、保護されていない鍵を暗号化データとともに格納することは、ドアマットの下に家屋鍵を置くようにするため、デバイスが損傷したときに静止鍵を保護しようとします.1つの例は、デバイスのオフラインデータダンプである可能性がある.デバイスのパスワードが分からないと、データは無駄になります..setRandomizedEncryptionRequired(true)オプションの有効化には、同じデータが2回目に暗号化された場合でも暗号化の出力が異なるように、十分なランダム化(毎回新しいランダムIVがある)が必要です.これにより、攻撃者が同じデータを供給することに基づいて暗号化に関する手がかりを得ることを防止できます.
もう1つの注意すべきオプションはsetUserAuthenticationValidWhileOnBody(boolean remainsValid)であり、デバイスがそれが人にいないことを検出すると、鍵がロックされる.
データの暗号化
鍵がKeyStoreに格納されるようになり、SecretKeyCipherオブジェクトを使用してデータを暗号化する方法を作成できます.これは、暗号化されたデータとデータの復号化に必要なランダムIVを含むHashMapを返す.その後、暗号化されたデータをIVとともにファイルまたは共有のプリファレンスに保存することができる.
private HashMap encrypt(final byte[] decryptedBytes)
{
    final HashMap map = new HashMap();
    try
    {
        //Get the key
        final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
        keyStore.load(null);
        final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry)keyStore.getEntry("MyKeyAlias", null);
        final SecretKey secretKey = secretKeyEntry.getSecretKey();

        //Encrypt data
        final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        final byte[] ivBytes = cipher.getIV();
        final byte[] encryptedBytes = cipher.doFinal(decryptedBytes);
        map.put("iv", ivBytes);
        map.put("encrypted", encryptedBytes);
    }
    catch (Throwable e)
    {
        e.printStackTrace();
    }

    return map;
}

バイト配列に復号
復号化には、反対の操作が適用されます.DECRYPT_MODE定数を使用してCipherオブジェクトを初期化し、復号化されたbyte[]配列を返します.
private byte[] decrypt(final HashMap map)
{
    byte[] decryptedBytes = null;
    try
    {
        //Get the key
        final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
        keyStore.load(null);
        final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry)keyStore.getEntry("MyKeyAlias", null);
        final SecretKey secretKey = secretKeyEntry.getSecretKey();

        //Extract info from map
        final byte[] encryptedBytes = map.get("encrypted");
        final byte[] ivBytes = map.get("iv");

        //Decrypt data
        final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        final GCMParameterSpec spec = new GCMParameterSpec(128, ivBytes);
        cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
        decryptedBytes = cipher.doFinal(encryptedBytes);
    }
    catch (Throwable e)
    {
        e.printStackTrace();
    }

    return decryptedBytes;
}

テスト例
サンプルをテストできます!
@TargetApi(Build.VERSION_CODES.M)
private void testEncryption()
{
    try
    {
        //Generate a key and store it in the KeyStore
        final KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
        final KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder("MyKeyAlias",
                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                //.setUserAuthenticationRequired(true) //requires lock screen, invalidated if lock screen is disabled
                //.setUserAuthenticationValidityDurationSeconds(120) //only available x seconds from password authentication. -1 requires finger print - every time
                .setRandomizedEncryptionRequired(true) //different ciphertext for same plaintext on each call
                .build();
        keyGenerator.init(keyGenParameterSpec);
        keyGenerator.generateKey();

        //Test
        final HashMap map = encrypt("My very sensitive string!".getBytes("UTF-8"));
        final byte[] decryptedBytes = decrypt(map);
        final String decryptedString = new String(decryptedBytes, "UTF-8");
        Log.e("MyApp", "The decrypted string is " + decryptedString);
    }
    catch (Throwable e)
    {
        e.printStackTrace();
    }
}

古いデバイスに対するRSA非対称鍵の使用
これはバージョンM以上のデータを格納する良いソリューションですが、アプリケーションが以前のバージョンをサポートしている場合はどうすればいいですか?MではAES対称鍵はサポートされておらず、RSA非対称鍵はサポートされている.これは、RSAキーと暗号化を使用して同じことを完了できることを意味します.
ここでの主な違いは、非対称鍵ペアが2つの鍵を含み、1つはプライベート鍵であり、もう1つはパブリック鍵であり、ここで、パブリック鍵はデータを暗号化し、プライベート鍵はデータを復号することである.KeyPairGeneratorSpecは、KEY_ALGORITHM_RSAおよび"AndroidKeyStore"プロバイダを使用して初期化されたKeyPairGeneratorに渡される.
private void testPreMEncryption()
{
    try
    {
        //Generate a keypair and store it in the KeyStore
        KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
        keyStore.load(null);

        Calendar start = Calendar.getInstance();
        Calendar end = Calendar.getInstance();
        end.add(Calendar.YEAR, 10);
        KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(this)
                .setAlias("MyKeyAlias")
                .setSubject(new X500Principal("CN=MyKeyName, O=Android Authority"))
                .setSerialNumber(new BigInteger(1024, new Random()))
                .setStartDate(start.getTime())
                .setEndDate(end.getTime())
                .setEncryptionRequired() //on API level 18, encrypted at rest, requires lock screen to be set up, changing lock screen removes key
                .build();
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");
        keyPairGenerator.initialize(spec);
        keyPairGenerator.generateKeyPair();

        //Encryption test
        final byte[] encryptedBytes = rsaEncrypt("My secret string!".getBytes("UTF-8"));
        final byte[] decryptedBytes = rsaDecrypt(encryptedBytes);
        final String decryptedString = new String(decryptedBytes, "UTF-8");
        Log.e("MyApp", "Decrypted string is " + decryptedString);
    }
    catch (Throwable e)
    {
        e.printStackTrace();
    }
}

暗号化のために、鍵ペアからRSAPublicKeyを取得し、Cipherオブジェクトとともに使用した.
public byte[] rsaEncrypt(final byte[] decryptedBytes)
{
    byte[] encryptedBytes = null;
    try
    {
        final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
        keyStore.load(null);
        final KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry("MyKeyAlias", null);
        final RSAPublicKey publicKey = (RSAPublicKey)privateKeyEntry.getCertificate().getPublicKey();

        final Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);

        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        final CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher);
        cipherOutputStream.write(decryptedBytes);
        cipherOutputStream.close();

        encryptedBytes = outputStream.toByteArray();

    }
    catch (Throwable e)
    {
        e.printStackTrace();
    }
    return encryptedBytes;
}
RSAPrivateKeyオブジェクトを使用して復号を完了します.
public byte[] rsaDecrypt(final byte[] encryptedBytes)
{
    byte[] decryptedBytes = null;
    try
    {
        final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
        keyStore.load(null);
        final KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry("MyKeyAlias", null);
        final RSAPrivateKey privateKey = (RSAPrivateKey)privateKeyEntry.getPrivateKey();

        final Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);

        final CipherInputStream cipherInputStream = new CipherInputStream(new ByteArrayInputStream(encryptedBytes), cipher);
        final ArrayList arrayList = new ArrayList<>();
        int nextByte;
        while ( (nextByte = cipherInputStream.read()) != -1 )
        {
            arrayList.add((byte)nextByte);
        }

        decryptedBytes = new byte[arrayList.size()];
        for(int i = 0; i < decryptedBytes.length; i++)
        {
            decryptedBytes[i] = arrayList.get(i);
        }
    }
    catch (Throwable e)
    {
        e.printStackTrace();
    }

    return decryptedBytes;
}

RSAについてはAESよりも暗号化が遅い.通常、共有プリファレンス文字列を保護する場合など、少量の情報に適しています.ただし、大量のデータを暗号化するパフォーマンスに問題がある場合は、この例ではAESキーのみを暗号化および格納するように変更できます.次に、前のチュートリアルで説明したAES暗号化を使用して、他のデータを暗号化します.新しいAES鍵を生成し、この例と互換性のあるbyte[]配列に変換できます.
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(256); //AES-256
SecretKey secretKey = keyGenerator.generateKey();
byte[] keyBytes = secretKey.getEncoded();

バイトから鍵を取り出すには、次の手順に従います.
SecretKey key = new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");

それはたくさんのコードです!すべての例を簡略化するために,徹底した異常処理を省略した.ただし、本番コードの場合、Throwableのすべてのケースを1つのcatch文でのみキャプチャすることは推奨されません.
結論
これにより、証明書と鍵の使用に関するチュートリアルが完了します.鍵とストレージに関するほとんどの困惑はAndroid OSの発展に関連していますが、アプリケーションがサポートするAPIレベルでは、どのソリューションを使用するかを選択できます.
静的データの保護のベストプラクティスについて説明した以上、次のチュートリアルでは、転送中のデータの保護に重点を置きます.
翻訳:https://code.tutsplus.com/tutorials/keys-credentials-and-storage-on-android--cms-30827