こんにちは、Habr! Jakob Jenkovによる記事「Java Cryptography」の翻訳を紹介します。
この出版物は、 Javaの暗号化の基礎を学びたい初心者向けの一連の記事からの最初のJava暗号化記事の翻訳です。
目次:
- Java暗号化
- 暗号
- メッセージダイジェスト
- Mac
- 署名
- キーペア
- キージェネレーター
- KeyPairGenerator
- キーストア
- キーツール
- 証明書
- CertificateFactory
- 証明書パス
Java暗号化
Java Cryptography APIは、Javaでデータを暗号化および復号化し、キー、署名を管理し、メッセージを認証(認証)し、暗号化ハッシュを計算する機能などを提供します。
この記事では、Java Cryptography APIを使用して安全な暗号化を必要とするさまざまなタスクを実行する方法の基本について説明します。
この記事では、暗号理論の基本については説明しません。 この情報はどこか別の場所で見る必要があります。
Java暗号化拡張機能
Java暗号化APIは、いわゆるJava Cryptography Extension (JCE)によって提供されます。 JCEは長い間Javaプラットフォームの一部でした。 JCEは元々、米国の暗号化技術に対する輸出制限のためにJavaから分離されていました。 したがって、最も強力な暗号化アルゴリズムは標準Javaプラットフォームに含まれていませんでした。 これらのより堅牢な暗号化アルゴリズムは、会社が米国にある場合に適用できますが、他の場合は、より弱いアルゴリズムを使用するか、独自の暗号化アルゴリズムを実装してJCEに接続する必要があります。
2017年以降、米国での暗号化アルゴリズムのエクスポート規則は大幅に緩和されており、世界のほとんどでJava JCEを介して国際的な暗号化標準を使用できます。
Java暗号化アーキテクチャ
Java Cryptography Architecture(JCA)は、Javaの内部暗号化APIデザインの名前です。 JCAは、いくつかのコアクラスと汎用インターフェイスを中心に構成されています。 これらのインターフェイスの実際の機能は、サプライヤによって提供されます。 したがって、Cipherクラスを使用して一部のデータを暗号化および復号化できますが、暗号の特定の実装(暗号化アルゴリズム)は、使用する特定のプロバイダーによって異なります。
独自のプロバイダーを実装して接続することもできますが、これには注意する必要があります。 セキュリティホールなしで暗号化を正しく実装することは困難です! 何をしているのかわからない場合は、組み込みのJavaプロバイダーを使用するか、Bouncy Castleなどの信頼できるプロバイダーを使用することをお勧めします。
主なクラスとインターフェース
Java暗号化APIは、次のJavaパッケージで構成されています。
- java.security
- java.security.cert
- java.security.spec
- java.security.interfaces
- javax.crypto
- javax.crypto.spec
- javax.crypto.interfaces
これらのパッケージの主なクラスとインターフェースは次のとおりです。
- プロバイダー
- SecureRandom
- 暗号
- メッセージダイジェスト
- 署名
- Mac
- AlgorithmParameters
- AlgorithmParameterGenerator
- キーファクトリー
- SecretKeyFactory
- KeyPairGenerator
- キージェネレーター
- キーアグリーメント
- キーストア
- CertificateFactory
- CertPathBuilder
- CertPathValidator
- CertStore
プロバイダー
Providerクラス(java.security.Provider)は、Java暗号化APIの中心的なクラスです。 Java暗号化APIを使用するには、暗号化プロバイダーをインストールする必要があります。 Java SDKには、独自の暗号化プロバイダーが付属しています。 暗号化プロバイダーを明示的に設定しない限り、デフォルトのプロバイダーが使用されます。 ただし、この暗号化プロバイダーは、使用する暗号化アルゴリズムをサポートしていない場合があります。 したがって、独自の暗号化プロバイダーをインストールする必要がある場合があります。
Java暗号化APIの最も人気のある暗号化プロバイダーの1つは、Bouncy Castleと呼ばれます。 BouncyCastleProviderが暗号化プロバイダーとして設定されている例を次に示します。
import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.Security; public class ProviderExample { public static void main(String[] args) { Security.addProvider(new BouncyCastleProvider()); } }
暗号
Cipherクラス(javax.crypto.Cipher)は暗号アルゴリズムを表します。 暗号化は、データの暗号化と復号化の両方に使用できます。 Cipherクラスの詳細については、以下のセクションで簡単に説明します。
内部使用にAES暗号化アルゴリズムを使用する暗号クラスのインスタンスを作成します。
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
Cipher.getInstance(...)メソッドは、使用する暗号化アルゴリズムとその他のアルゴリズムパラメータを決定する文字列を受け入れます。
上記の例では:
- AES-暗号化アルゴリズム
- CBCは、AESアルゴリズムが機能するモードです。
- PKCS5Paddingは、AESアルゴリズムが暗号化のためにデータの最後のバイトを処理する方法です。 これが正確に意味することは、この記事ではなく、暗号化マニュアル全体を見てください。
暗号の初期化
暗号インスタンスを使用する前に、初期化する必要があります。 暗号インスタンスは、 init()メソッドを呼び出すことにより初期化されます。 init()メソッドは2つのパラメーターを取ります。
- モード-暗号化/復号化
- キー
最初のパラメーターは、暗号インスタンスの操作モード、つまりデータの暗号化または復号化を示します。 2番目のパラメーターは、データの暗号化または復号化に使用するキーを示します。
例:
byte[] keyBytes = new byte[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}; String algorithm = "RawBytes"; SecretKeySpec key = new SecretKeySpec(keyBytes, algorithm); cipher.init(Cipher.ENCRYPT_MODE, key);
この例のキー生成方法は安全ではないため、実際には使用しないでください。 次のセクションのこの記事では、キーをより安全に作成する方法について説明します。
暗号インスタンスを初期化してデータを復号化するには、Cipher.DECRYPT_MODEを使用する必要があります。次に例を示します。
cipher.init(Cipher.DECRYPT_MODE, key);
データの暗号化または復号化
暗号を初期化した後、 update()またはdoFinal()メソッドを呼び出すことにより、データの暗号化または復号化を開始できます。 update()メソッドは、データの一部を暗号化または復号化する場合に使用されます。 doFinal()メソッドは、最後のデータを暗号化するとき、またはdoFinal()に渡すデータブロックが暗号化用の単一データセットであるときに呼び出されます。
doFinal()メソッドを使用したデータ暗号化の例:
byte[] plainText = "abcdefghijklmnopqrstuvwxyz".getBytes("UTF-8"); byte[] cipherText = cipher.doFinal(plainText);
データを復号化するには、暗号テキスト(データ)をdoFinal()またはdoUpdate()メソッドに渡す必要があります。
キー
データを暗号化または復号化するには、キーが必要です。 使用される暗号化アルゴリズムのタイプに応じて、キーには2つのタイプがあります。
- 対称キー
- 非対称キー
対称キーは、対称暗号化アルゴリズムに使用されます。 対称暗号化アルゴリズムは、暗号化と復号化に同じキーを使用します。
非対称キーは、非対称暗号化アルゴリズムに使用されます。 非対称暗号化アルゴリズムは、暗号化に1つのキーを使用し、復号化に別のキーを使用します。 公開鍵と秘密鍵の暗号化アルゴリズムは、非対称暗号化アルゴリズムの例です。
どういうわけか、データを復号化する必要がある当事者は、データを復号化するために必要なキーを知っている必要があります。 デクリプターがデータ暗号化の関係者でない場合、2つの関係者はキーについて合意するか、キーを交換する必要があります。 これはキー交換と呼ばれます。
キーセキュリティ
攻撃者が暗号化キーを簡単に取得できないように、キーは推測しにくいものでなければなりません。 Cipherクラスに関する前のセクションの例では、非常に単純なハードコードされたキーが使用されました。 実際には、これを行う価値はありません。 当事者のキーが推測しやすい場合、攻撃者は暗号化されたデータを簡単に解読し、場合によっては自分で偽のメッセージを作成することができます。 推測しにくいキーを作成することが重要です。 したがって、キーはランダムバイトで構成する必要があります。 ランダムなバイトが多いほど、組み合わせが多くなるため、推測が難しくなります。
キー生成
ランダム暗号化キーを生成するには、Java KeyGeneratorクラスを使用できます。 KeyGeneratorの詳細については、次の章で説明します。ここでは、その使用の小さな例を示します。
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); SecureRandom secureRandom = new SecureRandom(); int keyBitSize = 256; keyGenerator.init(keyBitSize, secureRandom); SecretKey secretKey = keyGenerator.generateKey();
結果のSecretKeyインスタンスは、たとえば次のようにCipher.init()メソッドに渡すことができます。
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
キーペアの生成
非対称暗号化アルゴリズムは、公開鍵と秘密鍵で構成される鍵ペアを使用して、データを暗号化および復号化します。 非対称キーペアを作成するには、KeyPairGenerator(java.security.KeyPairGenerator)を使用できます。 KeyPairGeneratorについては、次の章で詳しく説明します。以下は、Java KeyPairGeneratorを使用した簡単な例です。
SecureRandom secureRandom = new SecureRandom(); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA"); KeyPair keyPair = keyPairGenerator.generateKeyPair();
キーストア
Java KeyStoreは、キーを含むことができるデータベースです。 Java KeyStoreは、KeyStoreクラス(java.security.KeyStore)で表されます。 キーストアには、次のタイプのキーを含めることができます。
- 秘密鍵
- 公開鍵と証明書(公開鍵+証明書)
- 秘密鍵
秘密鍵と公開鍵は、非対称暗号化で使用されます。 公開鍵には証明書が関連付けられている場合があります。 証明書は、公開鍵を所有していると主張する個人、組織、またはデバイスの身元を証明する文書です。
証明書は通常、証明書として証明書利用者によってデジタル署名されます。
秘密鍵は対称暗号化で使用されますが、KeyStoreクラスは非常に複雑であるため、Java KeyStoreの別の章で後ほど詳しく説明します。
キー管理ツール(Keytool)
Java Keytoolは、Java KeyStoreファイルを操作できるコマンドラインツールです。 Keytoolは、KeyStoreファイルでキーペアを生成し、証明書をエクスポートし、証明書をKeyStoreおよびその他の機能にインポートできます。 KeytoolにはJavaインストールが付属しています。 Keytoolについては、Java Keytoolに関する別の章で詳しく説明します。
メッセージダイジェスト
暗号化されたデータを相手側から受け取ったときに、誰も暗号化されたデータを変更していないことを確認できますか?
通常、ソリューションは、データを暗号化する前にデータからメッセージダイジェストを計算し、データとメッセージダイジェストの両方を暗号化し、ネットワーク経由で送信することです。 メッセージダイジェストは、メッセージデータに基づいて計算されたハッシュ値です。 暗号化されたデータで少なくとも1バイトが変更されると、データから計算されるメッセージダイジェストも変更されます。
暗号化されたデータを受け取ったら、それを復号化し、それらからメッセージダイジェストを計算し、計算されたメッセージダイジェストを暗号化されたデータとともに送信されたメッセージのダイジェストと比較します。 2つのメッセージダイジェストが同じ場合、データが変更されていない可能性が高い(ただし、100%ではない)。
Java MessageDigest(java.security.MessageDigest)を使用して、メッセージダイジェストを計算できます。 MessageDigestのインスタンスを作成するには、 MessageDigest.getInstance()メソッドが呼び出されます。 いくつかの異なるメッセージダイジェストアルゴリズムがあります。 MessageDigestインスタンスを作成するときに使用するアルゴリズムを指定する必要があります。 MessageDigestの操作については、Java MessageDigestの章で詳しく説明します。
MessageDigestクラスの簡単な紹介:
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
この例では、SHA-256内部暗号化ハッシュアルゴリズムを使用してメッセージダイジェストを計算するMessageDigestのインスタンスを作成します。
一部のデータのメッセージダイジェストを計算するには、 update()またはdigest()メソッドを呼び出します。 update()メソッドは複数回呼び出すことができ、メッセージダイジェストはオブジェクト内で更新されます。 メッセージダイジェストに含めるすべてのデータを渡したら、 digest()を呼び出してメッセージダイジェストの概要を取得します。
update()を数回呼び出し、その後にdigest()を呼び出す例:
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); byte[] data1 = "0123456789".getBytes("UTF-8"); byte[] data2 = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); messageDigest.update(data1); messageDigest.update(data2); byte[] digest = messageDigest.digest();
また、 ダイジェスト()を 1回呼び出して、すべてのデータを渡してメッセージダイジェストを計算することもできます。 例:
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); byte[] data1 = "0123456789".getBytes("UTF-8"); byte[] digest = messageDigest.digest(data1);
メッセージ認証コード(MAC)
Java Macクラスは、メッセージからMAC(メッセージ認証コード)を作成するために使用されます。 MACはメッセージダイジェストに似ていますが、追加のキーを使用してメッセージダイジェストを暗号化します。 ソースデータとキーの両方のみを使用して、MACを確認できます。 したがって、MACは、メッセージダイジェストよりもデータブロックを変更から保護するためのより安全な方法です。 Macクラスについては、Java Macの章で詳しく説明し、その後に簡単な紹介が続きます。
Java Macインスタンスは、 Mac.getInstance()メソッドを呼び出して、パラメーターとして使用されるアルゴリズムの名前を渡すことで作成されます。 これは次のようなものです。
Mac mac = Mac.getInstance("HmacSHA256");
データからMACを作成する前に、Macインスタンスをキーで初期化する必要があります。 キーを使用してMacインスタンスを初期化する例を次に示します。
byte[] keyBytes = new byte[]{0,1,2,3,4,5,6,7,8 ,9,10,11,12,13,14,15}; String algorithm = "RawBytes"; SecretKeySpec key = new SecretKeySpec(keyBytes, algorithm); mac.init(key);
Macインスタンスを初期化した後、 update()およびdoFinal()メソッドを呼び出すことにより、データからMACを計算できます。 MACを計算するためのすべてのデータがある場合、すぐにdoFinal()メソッドを呼び出すことができます。 これは次のようなものです。
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); byte[] data2 = "0123456789".getBytes("UTF-8"); mac.update(data); mac.update(data2); byte[] macBytes = mac.doFinal();
署名
Signatureクラス(java.security.Signature)は、データにデジタル署名するために使用されます。 データが署名されると、このデータからデジタル署名が作成されます。 したがって、署名はデータから分離されます。
デジタル署名は、データからメッセージダイジェスト(ハッシュ)を作成し、データに署名する必要のあるデバイス、個人、または組織の秘密キーでこのメッセージダイジェストを暗号化することによって作成されます。 暗号化されたメッセージのダイジェストはデジタル署名と呼ばれます。
Signatureのインスタンスを作成するには、 Signature.getInstance(...)メソッドが呼び出されます。
Signature signature = Signature.getInstance("SHA256WithDSA");
データ署名
データに署名するには、initSign(...)メソッドを呼び出し、プライベートキーを渡してデータに署名することにより、署名モードで署名インスタンスを初期化する必要があります。 署名モードで署名インスタンスを初期化する例:
signature.initSign(keyPair.getPrivate(), secureRandom);
署名インスタンスを初期化した後、それを使用してデータに署名できます。 これは、update()メソッドを呼び出して、署名データをパラメーターとして渡すことで実行されます。 update()メソッドを数回呼び出して、署名を作成するためのデータを補足できます。 すべてのデータをupdate()メソッドに転送した後、sign()メソッドを呼び出してデジタル署名を取得します。 これは次のようなものです。
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); signature.update(data); byte[] digitalSignature = signature.sign();
署名検証
署名を検証するには、 initVerify(...)メソッドを呼び出し、署名の検証に使用される公開キーをパラメーターとして渡すことにより、検証モードで署名インスタンスを初期化する必要があります。 検証モードで署名インスタンスを初期化する例は次のようになります。
Signature signature = Signature.getInstance("SHA256WithDSA"); signature.initVerify(keyPair.getPublic());
検証モードで初期化した後、署名されたデータはupdate()メソッドに渡されます。 verify()メソッドの呼び出しは、署名を検証できるかどうかに応じてtrueまたはfalseを返します 。 署名の検証は次のとおりです。
byte[] data2 = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); signature2.update(data2); boolean verified = signature2.verify(digitalSignature);
完全な署名と検証の例
SecureRandom secureRandom = new SecureRandom(); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA"); KeyPair keyPair = keyPairGenerator.generateKeyPair(); Signature signature = Signature.getInstance("SHA256WithDSA"); signature.initSign(keyPair.getPrivate(), secureRandom); byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); signature.update(data); byte[] digitalSignature = signature.sign(); Signature signature2 = Signature.getInstance("SHA256WithDSA"); signature2.initVerify(keyPair.getPublic()); signature2.update(data); boolean verified = signature2.verify(digitalSignature); System.out.println("verified = " + verified);