web-dev-qa-db-ja.com

ファイルを暗号化するためにAESで使用されるbcrypt / sha256キーのセキュリティ

Pythonで安全なハッシュおよび暗号化アルゴリズムを使用してファイルを暗号化しようと思っています。過去にbcryptを使用したことがあるので、パスフレーズ計算機に使用し、出力をSHA256に渡して32バイトのデータを取得し、それをAESで使用してファイルを暗号化/復号化することにしました。

#!/usr/bin/env python

from argparse import ArgumentParser
from bcrypt import gensalt, hashpw
from Crypto.Cipher import AES
from hashlib import sha256

import os, struct, sys

def main():
    parser = ArgumentParser(description = "Encrypts or decrypts a file using " +
            "bcrypt for the password and triple AES for file encryption.")
    parser.add_argument('-p', '--passphrase', required = True, 
            help = "The passphrase to use for encryption.")
    parser.add_argument('-i', '--input', required = True,
            help = "The input file for encryption / decryption.")
    parser.add_argument('-o', '--output', required = True,
            help = "The output file for encryption / decryption.")
    parser.add_argument('-r', '--rounds', default = 10, 
            help = "The number of bcrypt rounds to use.")
    parser.add_argument('-s', '--salt', default = None,
            help = "The salt to use with bcrypt in decryption.")
    parser.add_argument('operation', choices = ('encrypt', 'decrypt'),
            help = "The operation to apply, whether to encrypt or decrypt data.")

    parameters = parser.parse_args()

    if parameters.operation == 'encrypt':
        encrypt(parameters.input, parameters.output, parameters.passphrase,
                parameters.rounds)
    Elif parameters.operation == 'decrypt':
        decrypt(parameters.input, parameters.output, parameters.passphrase,
                parameters.salt)

def encrypt(input_file, output_file, passphrase, rounds):
    bcrypt_salt = gensalt(rounds)
    bcrypt_passphrase = hashpw(passphrase, bcrypt_salt)
    passphrase_hash = sha256(bcrypt_passphrase).digest()

    print "Salt: %s" % (bcrypt_salt, )

    iv = os.urandom(16)

    cipher = AES.new(passphrase_hash, AES.MODE_CBC, iv)

    with open(input_file, 'rb') as infile:
        infile.seek(0, 2)
        input_size = infile.tell()

        infile.seek(0)  

        with open(output_file, 'wb') as outfile:
            outfile.write(struct.pack('<Q', input_size))
            outfile.write(iv)

            while True:
                chunk = infile.read(64 * 1024)
                if len(chunk) == 0:
                    break
                Elif len(chunk) % 16 != 0:
                    chunk += ' ' * (16 - len(chunk) % 16)

                outfile.write(cipher.encrypt(chunk))

    return bcrypt_salt

def decrypt(input_file, output_file, passphrase, salt):
    print "Salt: %s" % (salt,)

    bcrypt_passphrase = hashpw(passphrase, salt)
    passphrase_hash = sha256(bcrypt_passphrase).digest()

    with open(input_file, 'rb') as infile:
        input_size = struct.unpack('<Q', infile.read(struct.calcsize('Q')))[0]
        iv = infile.read(16)

        cipher = AES.new(passphrase_hash, AES.MODE_CBC, iv)

        with open(output_file, 'wb') as outfile:
            while True:
                chunk = infile.read(64 * 1024)
                if len(chunk) == 0:
                    break

                outfile.write(cipher.decrypt(chunk))

            outfile.truncate(input_size)

if __name__ == "__main__":
    main()

このような実装の潜在的な弱点は何ですか?

私が決定したことは、攻撃者が元のファイルサイズを簡単に特定できることですが、ファイルについてはあまり明らかになりません。 SHA-256は世界で最も優れたハッシュアルゴリズムではありませんが、bcryptパスワードをラップすると、すべての脅威が緩和されると私は信じるようになります。アルゴリズムにさらにラウンドを追加することにより、bcryptのセキュリティが徐々に向上するため、今のところbcryptを使用するのはかなり安全な方法のようです。

この実装にギャップホールはありますか?私は暗号学者ではありませんが、ここで使用されている3つのアルゴリズムのそれぞれの基本と目的は知っています。

6
Naftuli Kay

2分間見てから:

  • Bcryptの使用は健全です。 SHA-256 is最もよく知られているハッシュ関数の1つ(10年以上使用されており、広く展開されているが無傷のため)。

  • 暗号化されたファイルごとに新しいソルトを生成するので(これでいいのです!)、ファイルヘッダーに保存することは意味があります。そうすれば、それをパラメーターとして暗号化解除ルーチンに提供する必要がなくなります。ソルトは秘密である必要はありません(それはsaltであり、-keyではありません)が、暗号化ごとに新しいソルトを生成する必要があるため、外部処理が面倒な場合があります。 IVのカスタムヘッダーはすでにありますが、saltでも同じことができます。

    代替策:saltをヘッダーとして保存し、bcrypt出力から暗号化キーとIVの両方を生成します(SHA-256出力を2つの128ビットチャンクに分割します。最初のキーはキーで、2番目のキーはキーです) IV)。これにより、ヘッダーサイズが16バイトに維持されます。しかし、ランダムなIV(あなたのようにos.urandom()から)も悪くありません。

  • 暗号化を使用しますが、 [〜#〜] mac [〜#〜] はありません。 攻撃モデルはそれほど多くはありませんが、攻撃者はデータを観察できます(つまり、必要な機密性)が、データを変更することはできません(つまり、必要ありませんintegrity )。実際には、MACを追加することをお勧めします。理想的には、CBCを暗号化と整合性の両方を処理するモードに置き換えます( [〜#〜] gcm [〜#〜][〜#〜] eax [〜#〜] =)。

    サポートライブラリがそのようなモードを提供していない場合は、暗号化と [〜#〜] hmac [〜#〜] を組み合わせて「古いスタイル」にする必要があります。これは 行うのは簡単ではありません です。ベストプラクティスは、bcrypt出力から追加のMACキーを生成することです(暗号化キーを生成する場合、IV and MACキー、SHA-512などのより大きなハッシュ関数が必要になります)。 IVとencryptedデータを連結したMAC(パスフレーズからIVを生成する場合にのみIVをスキップできますが、MAC入力に含める方が安全です)。復号化の際は、最初にMACを確認し、次にMACが一致する場合のみ復号化に進みます。

  • あなた可能性アルゴリズムの俊敏性のためのいくつかの規定を含める必要があります。つまり、使用するアルゴリズムの組み合わせを識別するバイト値をヘッダーに追加します(bcrypt、SHA-256、AES + CBC、HMAC/SHA-256) )。これにより、既存のファイルとのコードの互換性を損なうことなく、後で別のアルゴリズムセットに切り替えることができます。その場合は、この「アルゴリズム識別子バイト」をMACの入力に追加することを忘れないでください。

  • パディングがあいまいです。復号化すると、最後にいくつかのスペース文字が見つかりますが、余分なスペースが入力ファイルの一部であったのか、暗号化関数によって追加されたのかはわかりません。そして、確かに、復号化するとスペースが残ります。これは特定の使用シナリオでは問題にならない可能性がありますが、一般的な暗号化/復号化ツールとして、ファイルが変更なしで暗号化/復号化の旅を生き残ることができない場合、これは通常問題と見なされます。 PKCS#7パディング (PKCS#5パディングとも呼ばれます)が一般的に使用されます:kバイトを追加します(少なくとも1、最大16、合計の長さは16)の倍数で、すべてのバイトの値はkです。復号化したら、最後のバイトを見てください。追加されたパディングバイトの数がわかります。

9
Thomas Pornin