Qtの応用 - AES
概要
AESは、DESおよび3DESに代わって規格化された共通鍵暗号方式である。
ブロック長は128ビット、鍵長は128ビット・192ビット・256ビットが選択できる。
AESのソースコードは、Brian Gladman氏のWebサイトで公開されている。
アセンブラで記述されたもの(高速)とC言語で記述されたものが公開されている。
また、GithubにQt AESライブラリが公開されている。
ブロック暗号化モード
AESには、ブロック暗号化モードという機能がある。
最も単純なのがECBモードであり、データを16[byte]ごとに区切って暗号化する。
しかし、Birthday Attack等に弱いという欠点があるため、ECBモードは使用するべきではない。
- CBC
- 暗号文ブロック連鎖モード(Cipher Block Chaining)
- OFB
- 出力フィードバックモード(Oftput Feed Back)
- CFB
- 暗号フィードバックモード(Cipher Feed Back)
- ECB
- 暗号ブックモード(Electric Code Book)
この他にも、PCBCやCounter Method等の新しい暗号利用モードも考案されている。
ECBモード
ECBモードは、1ブロックずつ単純に処理する。
ただし、ECBモードはブロック単位処理の裏をかいた暗号文一致攻撃をに弱い欠点がある。
暗号ブロックが一致した場合は復号した平文は一致するため、重要な秘密が漏れる可能性が高いと考えられる。
特定の暗号ブロックに対応する平文ブロックが1度でも知られると、同一の値を持つ暗号ブロックは全て解読される。
実際には、暗号文全体のうち部分的な平文は状況的に推測できるということは少なくないため、長い平文を暗号化する場合は非推奨である。
また、ブロック単位の差し替え等の暗号文改竄攻撃にも弱いという問題がある。
改竄攻撃を成功させるために、鍵を入手したり平文を正確に把握する必要が無いため、注意が必要である。
AESを使用した商用ソフトウェアにおいて、ECBモードが使用されることはほぼ無い。
CBCモード
CBCモードは、前の平文ブロックを暗号化した結果を、次の平文にXOR演算によって重ね合わせ、その結果に対して暗号化処理を行う。
最初のブロックを暗号化する場合、前の暗号文の最後のブロックを利用するか、または外部から与えた初期ベクトルを使用する。
前の暗号化結果が次のブロックに連鎖されるので連鎖モードと呼ぶ。
同一の平文から同一の暗号文が生成される可能性は極めて低いため、同一の平文が続く場合も安心して使用できる。
CBCモードに対する暗号文一致攻撃は、以下のように考えられる。
暗号文ブロックに対応する平文がとする時、偶然にも、であった場合、以下の攻撃が成立する。
それぞれの前の暗号文をCk_1、Cj_1とする時、以下が成立する。
また、CBCモードの手順から、以下が成立する。
すると、2つの平文の間に生じる差分は入手できてしまう。
しかし、このような攻撃が成功する確率は非常に稀であるといえる。
CBCモードの初期値は、攻撃者から見えても構わないことになっているが、毎回違う値を利用することが推奨されている。
CBCモードは、暗号化がランダムアクセスに適さないため、暗号化は先頭から順次行う必要がある。
また、復号がランダムアクセスに適しており、復号はランダムに選択した着目ブロックに対して操作できる。(1つ前の暗号文ブロックは必要である)
OFBモード
OFBモードは、初期ベクトルを暗号化して、それをまた暗号化して、次々と乱数を生成する。
その乱数列を、XOR演算によって平文に重ね合わせて、暗号化処理を行う。
したがって、ブロック暗号をストリーム暗号のように使用する。
OFBモードの初期値は、攻撃者から見えても構わないことになっているが、同じ鍵を使用する場合、必ず、初期値は毎回違う値を使用する。
同一の初期値と鍵の組み合わせから生じる乱数列は常に同一であるため、
平文と暗号文の組が1組でも攻撃者の手に渡ってしまうと、そこから乱数列が解ってしまい、
同一の初期値と鍵で暗号化した全ての暗号文が解読されてしまう。
OFBモードは、暗号化も復号化もランダムアクセスに適していないため、どちらも先頭から順次行う必要がある。
最近、人気のある暗号化モードにカウンタモード(またはカウンタメソッド)がある。
カウンタモードは、先頭からのブロック番号を求めてその値を暗号化して、その数値を乱数として使用する。
カウンタモードであれば、安全性が若干損なう可能性があるが、暗号化も復号もランダムに行えて便利である。
初期化ベクトル
カウンタモード (CTR)
CTRモードでは、カウンタ値を初期化ベクトルとして使用する。
各ブロックごとにカウンタをインクリメントすることにより、一意の値を生成する。
GCMモード
GCMモード等では、ナンス (使い捨ての数値) とカウンタを組み合わせて初期化ベクトルを生成する。
初期化ベクトル生成時の注意点
- 予測可能性
- 自動生成されたIV (初期化ベクトル) が予測可能であってはいけない。
- 例えば、単純なインクリメント方式は避けるべきである。
- 一意性
- 同じキーで同じIV (初期化ベクトル) を再利用しないことが重要である。
- 特にストリーム暗号的なモード (CTRモード, GCMモード) では致命的な脆弱性につながる。
- ランダム性
- 可能な限り、暗号学的に安全な乱数生成器を使用してIV (初期化ベクトル) を生成すべきである。
- 通信
- IV (初期化ベクトル) は、一般的に、暗号文と一緒に送信する必要がある。
- 多くの場合、暗号文の先頭に付加される。
- 長さ
- IV (初期化ベクトル) の長さは、使用する暗号化モードに応じて適切に設定する必要がある。
- 多くの場合、ブロックサイズと同じである。
自動生成されたIV (初期化ベクトル) を使用することにより、同じ平文でも毎回異なる暗号文が生成されて、セキュリティが向上する。
サンプルコード: CBCモード
以下の例では、QCrypto
クラスを使用して、CBCモードでの暗号化と復号を行っている。
- キー管理
- setKeyメソッドでパスワードからキーを生成する。
- キーが設定されていない場合は、暗号化・復号操作は失敗する。
- ただし、以下の例ではパスワードからキーを生成しているが、実務では、より安全なキー管理方法を使用すべきである。
- IV (初期化ベクトル) の自動生成
- 暗号化時において、自動的に新しいランダムなIV (初期化ベクトル) を生成して、IV (初期化ベクトル) を暗号文の先頭に付加して送信する。
- 復号時において、暗号文からIV (初期化ベクトル) を自動的に取り出して使用する。
- パディング
- PKCS7パディングを使用しており、復号時にパディングの妥当性を確認する。
// AES.hファイル
#ifndef AES_H
#define AES_H
#include <QObject>
#include <QByteArray>
class AES : public QObject
{
Q_OBJECT
private: // Variables
QByteArray m_key;
QString m_lastError;
private: // Methods
QByteArray generateKey(const QString &password);
QByteArray generateIV();
QByteArray padData(const QByteArray &data);
QByteArray unpadData(const QByteArray &data);
public: // Methods
explicit AES(QObject *parent = nullptr);
~AES();
bool setKey(const QString &password);
QByteArray encrypt(const QByteArray &plaintext);
QByteArray decrypt(const QByteArray &ciphertext);
QString lastError() const;
};
#endif // AES_H
// AES.cppファイル
#include <QCryptographicHash>
#include <openssl/aes.h>
#include <openssl/rand.h>
#include <openssl/err.h>
#include "AES.h"
AES::AES(QObject *parent) : QObject(parent)
{
OpenSSL_add_all_algorithms();
}
AES::~AES()
{
EVP_cleanup();
}
bool AES::setKey(const QString &password)
{
m_key = generateKey(password);
return !m_key.isEmpty();
}
QByteArray AES::encrypt(const QByteArray &plaintext)
{
if (m_key.isEmpty()) {
m_lastError = "キーがセットされていません";
return QByteArray();
}
QByteArray iv = generateIV();
QByteArray paddedText = padData(plaintext);
QByteArray ciphertext(paddedText.size(), 0);
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx) {
m_lastError = "暗号化コンテキストの生成に失敗";
return QByteArray();
}
if (EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL,
reinterpret_cast<const unsigned char*>(m_key.constData()),
reinterpret_cast<const unsigned char*>(iv.constData())) != 1) {
m_lastError = "Failed to initialize encryption";
EVP_CIPHER_CTX_free(ctx);
return QByteArray();
}
int len;
int ciphertext_len;
if (EVP_EncryptUpdate(ctx, reinterpret_cast<unsigned char*>(ciphertext.data()),
&len, reinterpret_cast<const unsigned char*>(paddedText.constData()),
paddedText.size()) != 1) {
m_lastError = "データの暗号化に失敗";
EVP_CIPHER_CTX_free(ctx);
return QByteArray();
}
ciphertext_len = len;
if (EVP_EncryptFinal_ex(ctx, reinterpret_cast<unsigned char*>(ciphertext.data()) + len, &len) != 1) {
m_lastError = "暗号化のファイナライズに失敗";
EVP_CIPHER_CTX_free(ctx);
return QByteArray();
}
ciphertext_len += len;
EVP_CIPHER_CTX_free(ctx);
return iv + ciphertext.left(ciphertext_len);
}
QByteArray AES::decrypt(const QByteArray &ciphertext)
{
if (m_key.isEmpty()) {
m_lastError = "キーがセットされていません";
return QByteArray();
}
if (ciphertext.size() < AES_BLOCK_SIZE) {
m_lastError = "暗号文のサイズが無効";
return QByteArray();
}
QByteArray iv = ciphertext.left(AES_BLOCK_SIZE);
QByteArray actualCiphertext = ciphertext.mid(AES_BLOCK_SIZE);
QByteArray plaintext(actualCiphertext.size(), 0);
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx) {
m_lastError = "暗号化コンテキストの生成に失敗";
return QByteArray();
}
if (EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL,
reinterpret_cast<const unsigned char*>(m_key.constData()),
reinterpret_cast<const unsigned char*>(iv.constData())) != 1) {
m_lastError = "Failed to initialize decryption";
EVP_CIPHER_CTX_free(ctx);
return QByteArray();
}
int len;
int plaintext_len;
if (EVP_DecryptUpdate(ctx, reinterpret_cast<unsigned char*>(plaintext.data()),
&len, reinterpret_cast<const unsigned char*>(actualCiphertext.constData()),
actualCiphertext.size()) != 1) {
m_lastError = "データの復号に失敗";
EVP_CIPHER_CTX_free(ctx);
return QByteArray();
}
plaintext_len = len;
if (EVP_DecryptFinal_ex(ctx, reinterpret_cast<unsigned char*>(plaintext.data()) + len, &len) != 1) {
m_lastError = "復号のファイナライズに失敗";
EVP_CIPHER_CTX_free(ctx);
return QByteArray();
}
plaintext_len += len;
EVP_CIPHER_CTX_free(ctx);
return unpadData(plaintext.left(plaintext_len));
}
QString AES::lastError() const
{
return m_lastError;
}
QByteArray AES::generateKey(const QString &password)
{
return QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha256);
}
QByteArray AES::generateIV()
{
QByteArray iv(AES_BLOCK_SIZE, 0);
if (RAND_bytes(reinterpret_cast<unsigned char*>(iv.data()), AES_BLOCK_SIZE) != 1) {
m_lastError = "初期化ベクトルの生成に失敗";
return QByteArray();
}
return iv;
}
QByteArray AES::padData(const QByteArray &data)
{
int padding = AES_BLOCK_SIZE - (data.size() % AES_BLOCK_SIZE);
return data + QByteArray(padding, padding);
}
QByteArray AES::unpadData(const QByteArray &data)
{
if (data.isEmpty()) {
return data;
}
int padding = data[data.size() - 1];
if (padding > AES_BLOCK_SIZE || padding < 1) {
m_lastError = "パディングが無効";
return QByteArray();
}
return data.left(data.size() - padding);
}
#include <QCoreApplication>
#include <QDebug>
#include "AES.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
AES aes;
// 暗号化 (AES CBC)
if (!aes.setKey("<任意の暗号化パスワード>")) {
qDebug() << "Failed to set key:" << aes.lastError();
return;
}
QByteArray plaintext = "Hello, World! This is a secret message.";
QByteArray ciphertext = aes.encrypt(plaintext);
if (ciphertext.isEmpty()) {
qDebug() << "暗号化に失敗: " << aes.lastError();
return;
}
// 復号 (AES CBC)
QByteArray decrypted = aes.decrypt(ciphertext);
if (decrypted.isEmpty()) {
qDebug() << "復号に失敗: " << aes.lastError();
return;
}
qDebug() << "復号したデータ: " << QString::fromUtf8(decrypted);
return a.exec();
}
※注意
- 暗号化時は、IV (初期化ベクトル) を自動生成して、暗号文の先頭に追加している。
- 復号時は、暗号文の先頭からIV (初期化ベクトル) を取り出して使用する。
- パスワードからキーを生成しているが、実務ではより安全なキー管理方法を使用すべきである。
- エラーハンドリングにおいて、入出力の検証を追加することを推奨する。
Crypto++
Crypto++とは
AES (またはその他のブロック暗号) を使用する場合は、一般的に、ブロック暗号のインスタンスを持つモードを使用する。
例えば、CFBモードの場合は、以下のように記述する。
CFB_Mode<AES>::Encryption Encrypt; // CFB_Modeは、ブロック暗号をテンプレートパラメータとして受け取る
Crypto++のインストール
Crypto++ Libraryの公式Webサイトにアクセスして、暗号化ライブラリのソースコードをダウンロードする。
ダウンロードしたファイルを解凍する。
unzip cryptopp<バージョン>.zip cd cryptopp<バージョン>
または、Githubからソースコードをダウンロードすることもできる。
git clone https://github.com/weidai11/cryptopp.git cd cryptopp
Crypto++をビルドおよびインストールする。
make -j $(nproc) static dynamic CXX=/<GCCのインストールディレクトリ>/g++ make test make install DESTDIR=<暗号化ライブラリのインストールディレクトリ>
暗号化ライブラリの使用手順は、以下のWebサイトを参照すること。
https://www.cryptopp.com/wiki/Advanced_Encryption_Standard
ブロック暗号の参照
外部のブロック暗号オブジェクトのインスタンスではなく、そのオブジェクトへの参照を保持するモードオブジェクトを作成することもできる。
以下の例では、外部のAESオブジェクトでCFBモードを使用している。
AES::Encryption aesEncryption(key, AES::DEFAULT_KEYLENGTH);
CFB_Mode_ExternalCipher::Encryption cfbEncryption(aesEncryption, iv);
ExternalCipherモードは、WindowsでFIPSをサポートするために追加されたものであり、必要がない限り避けるべきである。
ECBおよびCBCモードの備考
ECBおよびCBCモードでは、データをブロックサイズの倍数で処理する必要がある。
または、StreamTransformationFilter
をモードオブジェクトにラッピングして、フィルタオブジェクトとして使用することもできる。
StreamTransformationFilter
は、データをブロックにバッファリングして、必要に応じてパディングを行う。
もし、フルブロックサイズで処理する(パディングを行わない)場合は、第4引数にStreamTransformationFilter
のNO_PADDING
を指定する。
StringSource(data, true, new StreamTransformationFilter(encryptor, new StringSink(result), NO_PADDING))
サンプルコード
以下の例では、AESが使用する鍵長の最小値、最大値、標準値をダンプしている。(2番目と3番目はパイプラインのフィルターを使用している)
パイプラインは高レベルの抽象化であり、入力のバッファリング、出力のバッファリング、パディングを処理する。
std::cout << "鍵長 : " << AES::DEFAULT_KEYLENGTH << std::endl;
std::cout << "鍵長(最小) : " << AES::MIN_KEYLENGTH << std::endl;
std::cout << "鍵長(最大) : " << AES::MAX_KEYLENGTH << std::endl;
std::cout << "ブロックサイズ : " << AES::BLOCKSIZE << std::endl;
// 出力
// 標準値の鍵長は128ビット(16バイト)
鍵長 : 16
鍵長(最小) : 16
鍵長(最大) : 32
ブロックサイズ : 16
以下の例では、CFBモードを使用して、暗号化および復号を行っている。
ECBやCBCではないため、データ長をAESのブロックサイズの倍数にする必要は無い。
AutoSeededRandomPool rnd;
// Generate a random key
SecByteBlock key(0x00, AES::DEFAULT_KEYLENGTH);
rnd.GenerateBlock(key, key.size());
// Generate a random IV
SecByteBlock iv(AES::BLOCKSIZE);
rnd.GenerateBlock(iv, iv.size());
byte plainText[] = "Hello! How are you.";
size_tmessageLen = std::strlen((char*)plainText) + 1;
// Encrypt
CFB_Mode<AES>::Encryption cfbEncryption(key, key.size(), iv);
cfbEncryption.ProcessData(plainText, plainText, messageLen);
// Decrypt
CFB_Mode<AES>::Decryption cfbDecryption(key, key.size(), iv);
cfbDecryption.ProcessData(plainText, plainText, messageLen);
ここでは、外部で生成された鍵と初期化ベクトル(IV)を使用して、暗号化器と復号器を取得する例を記述する。
その後の処理は、上記の例と同様に記述する。
SecByteBlock aes_key(16);
SecByteBlock iv(16);
// stub for how you really get it, e.g. reading it from a file, off of a
// network socket encrypted with an asymmetric cipher, or whatever
read_key(aes_key, aes_key.size());
// stub for how you really get it, e.g. filling it with random bytes or
// reading it from the other side of the socket since both sides have
// to use the same IV as well as the same key
read_initialization_vector(iv);
// the final argument is specific to CFB mode, and specifies the refeeding size in bytes.
// This invocation corresponds to Java's Cipher.getInstance("AES/CFB8/NoPadding")
auto enc = new CFB_Mode<AES>::Encryption(aes_key, sizeof(aes_key), iv, 1);
// the final argument is specific to CFB mode, and specifies the refeeding size in bytes.
// This invocation corresponds to Java's Cipher.getInstance("AES/CFB8/NoPadding")
auto dec = new CFB_Mode<AES>::Decryption(aes_key, sizeof(aes_key), iv, 1);
Qt AESライブラリ
Qt AESライブラリとは
Qt向けのポータブルなAES暗号化ライブラリである。
128 / 192 / 256ビットの鍵長をサポートしており、ECB、CBC、CFB、OFBモードおよび部分的なAES-NIをサポートしている。
Qt AESライブラリはフリーライセンスであり、商用 / 非商用を問わず、自由に使用、改変、販売してもよい。
また、ソースコードの公開義務も無い。
そのため、Qtの実行バイナリまたはQtプロジェクトにおいて、Qt AESライブラリを同梱して配布してもよい。
Qt AESライブラリのダウンロード
Qt AESライブラリのビルドに必要なライブラリをインストールする。
# SUSE sudo zypper install make cmake gcc gcc-c++ \ qt6-core-devel # Qt 6を使用する場合 qt6-test-devel # Qt AESライブラリのテストを行う場合 (Qt 6を使用する場合) libQt5Core-devel # Qt 5を使用する場合 libQt5Test-devel # Qt AESライブラリのテストを行う場合 (Qt 5を使用する場合)
Qt AESクラスのGuthubにアクセスして、ソースコードをダウンロードする。
ダウンロードしたファイルを解凍する。
tar xf Qt-AES-<バージョン>.tar.gz cd Qt-AES-<バージョン>
Qt AESライブラリをビルドおよびインストールする。
mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=<Qt AESライブラリのインストールディレクトリ> .. make -j $(nproc) make install
Qt AESライブラリのサンプルコード
Qt AESライブラリを使用するには、Qt Coreライブラリが必要となる。
また、テストを行う場合は、Qt Testライブラリも必要となる。
以下の例では、Qt AESライブラリを使用して、鍵長128[bit]のECBモードで暗号化している。
#include <QCryptographicHash>
#include "qaesencryption.h"
// AESによるデータの暗号化
QString Encrypt(QString data, QString key)
{
// 鍵長128ビット, ECBモード, ゼロパディング
QAESEncryption encryption(QAESEncryption::AES_128, QAESEncryption::ECB, QAESEncryption::ZERO);
// QCryptographicHashによる鍵の暗号化
QByteArray hashKey = QCryptographicHash::hash(key.toUtf8(), QCryptographicHash::Sha1);
// データの暗号化
QByteArray encodedText = encryption.encode(data.toUtf8(), hashKey);
// QByteArrayからQStringへ変換
// toBase64関数は削除できない
QString encodeTextStr = QString::fromLatin1(encodedText.toBase64());
return encodeTextStr;
}
以下の例では、Qt AESライブラリを使用して、鍵長128[bit]のECBモードで復号している。
#include <QCryptographicHash>
#include "qaesencryption.h"
// Qt AESライブラリを使用したデータの復号
QString decodedText(QString data, QString key)
{
// 鍵長128ビット, ECBモード, ゼロパディング
QAESEncryption encryption(QAESEncryption::AES_128, QAESEncryption::ECB, QAESEncryption::ZERO);
// QCryptographicHashによる鍵の暗号化
QByteArray hashKey = QCryptographicHash::hash(key.toUtf8(), QCryptographicHash::Sha1);
// データの復号
QByteArray decodedText = encryption.decode(QByteArray::fromBase64(data.toLatin1()), hashKey);
// QByteArrayからQStringへ変換
QString decodedTextStr = QString::fromLatin1(decodedText);
return decodedTextStr;
}