[この記事は Sergio Giro、ソフトウェア エンジニアによる Android Developers Blog の記事 "Security "Crypto" provider deprecated in Android N" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。]

random_droid 
Android アプリで SHA1PRNG アルゴリズムを利用して Crypto プロバイダからキーを導出している場合は、本物のキー導出関数を使うように変更する必要があります。既存のデータも暗号化し直す必要があるかもしれません。

Java 暗号化アーキテクチャを使用すると、次のような呼び出しによって暗号や疑似乱数ジェネレータのようなクラスのインスタンスを作成できます。
SomeClass.getInstance("SomeAlgorithm", "SomeProvider");
もっと単純に、次のように書くこともできます。
SomeClass.getInstance("SomeAlgorithm");
たとえば、次のような使い方が可能です。
Cipher.getInstance(“AES/CBC/PKCS5PADDING”);
SecureRandom.getInstance(“SHA1PRNG”);

Android でプロバイダを指定することは推奨されていません。通常、プロバイダを指定した Java Cryptography Extension(JCE)API の呼び出しを行うのは、プロバイダがアプリケーションに含まれている場合や、ProviderNotFoundException が発生した場合にアプリケーションで対応できる場合に限る必要があります。

残念なことに、多くのアプリは削除される「Crypto」プロバイダに依存しています。これはキー導出のアンチパターンです。

このプロバイダは、SecureRandom のインスタンス用の「SHA1PRNG」アルゴリズムの実装のみを提供するものでした。ここで問題になるのは、SHA1PRNG アルゴリズムは暗号学的に安全ではないことです。詳細に興味がある読者の方は、Yongge Want 氏と Tony Nicol 氏の『On statistical distance based testing of pseudo random sequences and experiments with PHP and Debian OpenSSL』のセクション 8.1 をご覧ください。バイナリ形式で考えた場合、「ランダム」シーケンスは 0 を返す可能性が高く、シードによってはさらにその傾向が強くなることが説明されています。

そのため、Android N では SHA1PRNG アルゴリズムと Crypto プロバイダの実装が廃止されます。数年前に公開された資格情報を安全に保存する暗号の使用という記事では、キー導出に SecureRandom を使用する際の問題が取り上げられています。しかし、この方法が継続的に使用されていることを考慮し、この問題を再検討することになりました。

このプロバイダは、パスワードをシードとして暗号化キーを導出するという誤った使われ方が一般的になっています。SHA1PRNG の実装には、出力を得る前に setSeed() を呼び出すとそれが固定されてしまうというバグがあります。パスワードをシードとし、「ランダム」な出力バイトを使用してキーを導出すると、このバグが発生します(この文で、カッコつきの「ランダム」は「予測可能で暗号学的に脆弱」という意味です)。そのようなキーがデータの暗号化や復号化に使われることになります。

次に、正しくキーを導出する方法と、安全ではないキーで暗号化されたデータを復号化する方法について説明します。廃止される SHA1PRNG 機能を使用するためのヘルパークラスを含む完全な例も掲載しています。これは、この方法を用いて復号化しなければ使うことができないデータを復号化することのみを目的としたものです。

キーは、次のように導出することができます。
  • ディスク上の AES キーを読み出す場合は、ただ実際のキーを格納するだけで構いません。余計なことをする必要はまったくありません。AES 用の SecretKey は、次のようにしてバイト列から取得できます。
    SecretKey key = new SecretKeySpec(keyBytes, "AES");
  • キーを導出するためにパスワードを使用している場合は、Nikolay Elenkov のすばらしいチュートリアルに従います。経験上、ソルトのサイズは出力されるキーのサイズとそろえておくとよいと言われていることに注意しましょう。以下は、一例です。
   /* User types in their password: */  
   String password = "password";  

   /* Store these things on disk used to derive key later: */  
   int iterationCount = 1000;  
   int saltLength = 32; // bytes; should be the same size
              as the output (256 / 8 = 32)  
   int keyLength = 256; // 256-bits for AES-256, 128-bits for AES-128, etc  
   byte[] salt; // Should be of saltLength  

   /* When first creating the key, obtain a salt with this: */  
   SecureRandom random = new SecureRandom();  
   byte[] salt = new byte[saltLength];  
   random.nextBytes(salt);  

   /* Use this to derive the key from the password: */  
   KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt,  
              iterationCount, keyLength);  
   SecretKeyFactory keyFactory = SecretKeyFactory  
              .getInstance("PBKDF2WithHmacSHA1");  
   byte[] keyBytes = keyFactory.generateSecret(keySpec).getEncoded();  
   SecretKey key = new SecretKeySpec(keyBytes, "AES");  

これだけです。他には何もする必要はありません。

データ移行を簡単にするために、毎回パスワードから導出される安全でないキーで暗号化されているデータへの対応方法も紹介します。キーは、サンプルアプリのヘルパークラス InsecureSHA1PRNGKeyDerivator を使って導出できます。
 private static SecretKey deriveKeyInsecurely(String password, int
 keySizeInBytes) {  
    byte[] passwordBytes = password.getBytes(StandardCharsets.US_ASCII);  
    return new SecretKeySpec(  
            InsecureSHA1PRNGKeyDerivator.deriveInsecureKey(  
                     passwordBytes, keySizeInBytes),  
            "AES");  
 }  

その後、先ほどの説明にあった安全に導出したキーを使ってデータを再暗号化すれば、あとはもう安心です。

注 1: アプリを動作させるための一時措置として、SDK バージョン 23(Marshmallow 用の SDK バージョン)以下を対象としたアプリでは、インスタンスを作成できるようになっています。Android SDK では、Crypto プロバイダの存在に依存しないようにしてください。これは今後完全に削除される予定となっています。

注 2: 多くのシステムで SHA1PRNG アルゴリズムの存在が前提とされているため、プロバイダを指定せずに SHA1PRNG インスタンスをリクエストした場合、OpenSSLRandom のインスタンスが返されるようになっています。これは OpenSSL に由来する安全な乱数ソースです。


Posted by Yuichi Araki - Developer Relations Team