C#の例でストリーム暗号がどのように機能するか。 ワンタイムメモ帳からハッシュおよびCTRストリーム暗号へ

「ワンタイムメモ帳」のアイデアを発展させる記事の過程で、ハッシュ関数に基づくストリーム暗号を「発明」します。 カウンターモード暗号化CTRについて説明します。



「ハッシュ関数」および「ワンタイムメモ帳」という用語の知識は、読むために必要ではありません。



使い捨てノート



ワンタイムメモ帳では、暗号文はプレーンテキストにキーを重ねることで取得されます。 重複は、たとえばビット単位のXORを使用して実行できます。プレーンテキストの各ビットは、キーの対応する(同じ順序の)ビットとXORです。



各プレーンテキストビットは、同じキービットを順番に持つXORです






図1.各平文ビットは、同じキービットを順番に持つXORです



したがって、復号化を行うには、受信した暗号文をキーとXORで取得する必要があります。



var encoding = Encoding.GetEncoding(1251); var plainText = encoding.GetBytes("111111"); var key = encoding.GetBytes("123456"); // encrypt: key XOR plainText byte[] cipherText = new byte[plainText.Length]; new BitArray(key).Xor(new BitArray(plainText)) .CopyTo(cipherText,0); // decrypt: key XOR chipherText var decripted = new byte[cipherText.Length]; new BitArray(key).Xor(new BitArray(cipherText)) .CopyTo(decripted, 0); var decriptedStr = encoding.GetString(decripted);  1.    ,   ,      
      
      





キーがランダムで予測不能であればあるほど、それを拾うのは難しくなります。 実際の「ワンタイムノートブック」の場合、キーはランダムである必要があります。



 var plainText = encoding.GetBytes("111111"); var key = new byte[6]; using (var randomGenerator = new RNGCryptoServiceProvider()) randomGenerator.GetBytes(key);  2.     
      
      





キーは1回限りである必要があります



明らかに、キーの長さは暗号化されたテキストの長さ以上でなければなりません。 キーがオープンテキストよりも短い場合は、周期的に繰り返してみてください-図2:



キーはプレーンテキストよりも短いです。キーを繰り返して、暗号化されたすべてのテキストをカバーします






図2.鍵は平文よりも短い。 キーを繰り返して、暗号化されたすべてのテキストをカバーします



テキスト上でキーを重ねることをギャミングと呼びます。 この用語では、「ワンタイムノートブック」キーはガンマと呼ばれます。


ガンマサイクル法の例



 //  XOR //    data    XOR, //   static IEnumerable<byte> XORStrimmed(byte[] gamma, IEnumerable<byte> data) { var gammaIndex = 0; foreach (var bb in data) { // XOR yield return (byte)(bb ^ gamma[gammaIndex]); if (gammaIndex < gamma.Length - 1) gammaIndex++; else gammaIndex = 0; } }  3.   XOR   
      
      





テキストがキーよりも長い場合に何が起こるか見てみましょう。ガンマサイクルを繰り返します。



 var plainText = encoding.GetBytes("11111111111111111111111111111111111111111111111111"); var key = new byte[6]; using (var randomGenerator = new RNGCryptoServiceProvider()) randomGenerator.GetBytes(key); // encrypt: key XOR plainText var cipherText = XORStrimmed(key, plainText); // decrypt: key XOR chipherText var decripted = XORStrimmed(key, cipherText); var decriptedStr = encoding.GetString(decripted.ToArray());  4.      
      
      





暗号文がそれほど強くないことが判明したことに気付くために暗号作成者である必要はありません(少なくとも、平文の文字が繰り返されていることがわかります)。



ガンマを単純に繰り返す暗号文は不安定です






図3.ガンマの単純な繰り返しによる暗号文は不安定です



同じ理由で、ノートブックはワンタイムと呼ばれます。同じキーを2回使用することはできません。

例のこのようなクリアテキストは、説明のために選択されています。 優れた暗号は、そのような文字列であっても、パターンを繰り返すことなく暗号テキストを生成します。


ストリーム暗号



これまで見てきたように、ガンマを繰り返すべきではありません。 次のラウンドでガンマが終了するたびに、何らかの方法で新しいガンマを取得する必要があります。



ガンマが終了するたびに、新しいガンマを作成する必要があります。各ラウンドには独自のガンマがあります






図4.ガンマが終了するたびに、新しいガンマを作成する必要があります。 各ラウンドには独自のガンマがあります



また、ガンマを繰り返すだけでなく、できるだけランダムにする必要があることもわかっています。 ただし、各ラウンドで本当にランダムなガンマを生成すると、ワンタイムメモ帳が得られます。キーは暗号文のサイズになります。 これは必要なものではありません。



キー依存の擬似ランダムガンマ生成メカニズムが必要です。 キーを知って解読するとき、同じ色域を生成できるはずです。



ストリーム暗号スキーム






図5.ストリーム暗号スキーム



ジェネレータがすべてのプレーンテキストに対してキー依存の擬似ランダムガンマを作成すると、ストリーム暗号が取得されます。



カウンターモード暗号化CTR



補足図4:キーとラウンド数に応じてガンマを生成する関数が必要です。



各ラウンドのガンマは、キーとラウンドの数に依存する関数によって作成されます。






図6.各ラウンドのガンマは、キーとラウンドの数に応じた関数によって実行されます。



これは、カウンターモード暗号化CTRです。ガンマジェネレーターは、入力でラウンドのカウンターを受け取ります。



Counter Mode Encryption CTRを使用した3ラウンドのストリーム暗号(1つのコンポーネントを除き、以下を参照)






図7. Counter Mode Encryption CTRを使用した3ラウンドのストリーム暗号(1つのコンポーネントを除き、以下を参照)



ガンマジェネレーターとしてのハッシュ関数



ハッシュ関数は、プレーンテキストを特定の長さの擬似ランダムシーケンスに変換する関数です。 ハッシュ(ハッシュ関数の結果)を知っていると、プレーンテキストを復元することは不可能です。 プレーンテキストを1ビットでも変更すると、まったく異なる擬似ランダムハッシュになります。



すなわち ハッシュ関数をガンマジェネレータとして使用できます。



Gamma_ round = HASH(キー+ RoundIndex)



 private static IEnumerable<byte> XorCounterModeEncryptDecrypt(byte[] keyBytes, IEnumerable<byte> data) { if (keyBytes == null) throw new ArgumentNullException(nameof(keyBytes)); if (data == null) throw new ArgumentNullException(nameof(data)); int roundIndex = 0; byte[] roundGamma = null; int gammaIndex = 0; foreach (var d in data) { if (gammaIndex == 0) { // create gamma var counterBlock = keyBytes.Concat(BitConverter.GetBytes(roundIndex)).ToArray(); using (var sha = new SHA512Managed()) roundGamma = sha.ComputeHash(counterBlock); } yield return (byte)(d ^ roundGamma[gammaIndex]); if (gammaIndex < roundGamma.Length - 1) gammaIndex++; else { gammaIndex = 0; roundIndex++; } } // foreach }  5.   XOR,          
      
      





使用例



 var plainText = encoding.GetBytes("11111111111111111111111111111111111111111111111111"); var key = encoding.GetBytes("1123456"); // encrypt: key XOR plainText var cipherText = XorCounterModeEncryptDecrypt(key, plainText); // decrypt: key XOR chipherText var decripted = XorCounterModeEncryptDecript(key, cipherText); var decriptedStr = encoding.GetString(decripted.ToArray());  6. /   Counter Mode Encryption  
      
      





ノンス、HMAC



暗号の実装はまだ完了していません。 現在の実装では、同じプレーンテキストを持つ同じキーは常に同じ暗号文を生成します。 これは受け入れられません。同じ暗号化されたコマンド「緊急にビットコインを販売する」が通信チャネルを介して送信されると想像してください-これは冗談ですが、あなたはその考えを理解しました。



そのため、もう1つの要件が追加されます。暗号化のたびに、異なる暗号文を取得する必要があります。 私たちの場合、これはガンマジェネレーターを完成させる必要があることを意味します。



この問題は、いわゆるNonceによって解決されます。





Nonceを追加したCounter Mode Encryption CTRを使用した3ラウンドのストリーム暗号






図8. Nonceを追加したCounter Mode Encryption CTRを使用した3ラウンドのストリーム暗号



HMACは、いくつかの追加パラメーターへの依存関係をハッシュ関数に追加する方法です。 連結を使用して同じ結果(キャッシュをいくつかのパラメーターに依存させる)を達成しました。 すなわち 論理的に



HASH(RoundIndex +キー)



と同じ



HMAC(RoundIndex、キー)



結論は異なりますが、論理的には同じです。



 private static IEnumerable<byte> XorCounterModeEncryptDecrypt(byte[] keyBytes, byte[] nonceBytes, IEnumerable<byte> data) { if (keyBytes == null) throw new ArgumentNullException(nameof(keyBytes)); if (data == null) throw new ArgumentNullException(nameof(data)); if (nonceBytes == null) throw new ArgumentNullException(nameof(nonceBytes)); if (nonceBytes.Length < 4) throw new ArgumentOutOfRangeException(nameof(nonceBytes)); var nonce = new BitArray(nonceBytes); int roundIndex = 0; byte[] roundGamma = null; int gammaIndex = 0; foreach (var d in data) { if (gammaIndex == 0) { // create gamma // create counter block: Nonce + Counter // another way: Nonce XOR Counter (has some constraints) var counterBlock = nonceBytes.Concat(BitConverter.GetBytes(roundIndex)).ToArray(); using (var hmacSHA = new HMACSHA512(keyBytes)) roundGamma = hmacSHA.ComputeHash(counterBlock); } yield return (byte)(d ^ roundGamma[gammaIndex]); if (gammaIndex < roundGamma.Length - 1) gammaIndex++; else { gammaIndex = 0; roundIndex++; } } // foreach }  7.   XOR,          .  Nonce  HMAC
      
      





 var plainText = encoding.GetBytes("11111111111111111111111111111111111111111111111111"); var key = encoding.GetBytes("1123456"); var nonce = new byte[4]; using (var randomGenerator = new RNGCryptoServiceProvider()) randomGenerator.GetBytes(nonce); // encrypt: key XOR plainText var cipherText = nonce.Concat( XorCounterModeEncryptDecrypt(key, nonce, plainText) ); // decript: key XOR chipherText var decripted = XorCounterModeEncryptDecrypt( keyBytes: key, nonceBytes: cipherText.Take(4).ToArray(), data: cipherText.Skip(4)); var decriptedStr = encoding.GetString(decripted.ToArray());  8.  
      
      





アルゴリズム



結果のアルゴリズムを簡単に説明します。



  1. ランダムナンスを作る。 One Nonceはすべてのラウンドで使用されます。
  2. 各ラウンドで、ガンマを生成するために使用される一意のCounterBlockを計算します。



    CounterBlock = Nonce + Round Number
  3. ラウンドのガンマを生成します



    ガンマ= HMAC(CounterBlock、Key)
  4. ビット単位のXORを使用したプレーンテキストのガンマオーバーレイ
  5. ガンマが終了すると、次のラウンドが始まります。 ラウンド数を1増やす-新しいラウンドが新しい色域を取得する


免責事項



上記の自転車は教育目的のみです。 ストリーム暗号でハッシュ関数を使用するという考え方は新しいものではありません(以下を参照)。 私の知る限り、そのようなスキームの耐久性について明確な意見はありません。 おそらく、この質問は単に関係ないだけです-ハッシュの動作はより遅くなります。



このアイデアを擁護するために、pgpru.comでの最初の議論からコメントを提供ます

ハッシュをPRFとして使用して、暗号化されたデータをXORitするストリームを作成することは新しいことではありません。



たとえば、SHA3ファイナリストの1つであるSkeinハッシュ関数(http://www.skein-hash.info/sites/default/files/skein1.3.pdf)の説明では、ハッシュPDF自体に同様の使用例が説明されています。



不正確、エラーが表示された場合は、理解して処理してください。 私は暗号作成者ではありません。情報セキュリティは、プロジェクト開発の適用目的にのみ使用され、情報セキュリティの過剰な要件はありません。



参照資料



GitHub StreamCipherOnHashAndCTRMode



pgpru.comでの最初のディスカッション

ブロック暗号モードの操作

ハッシュ関数で5つの動作モードを実装する

情報理論とワンタイムノート

ストリーム暗号



All Articles