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つ前の暗号文ブロックは必要である)
CFBモード
CFBモード (Cipher Feedback Mode) は、AES暗号化アルゴリズムで使用されるブロック暗号の運用モードの1つである。
このモードは、ストリーム暗号のように機能して、平文を任意のビット長で暗号化することができる。
CFBモードの主な特徴は、暗号文の生成時に、前の暗号ブロックの一部を使用することである。
これにより、各ブロックの暗号化が前のブロックに依存するため、暗号文にパターンが現れにくくなる。
動作の流れを以下に示す。
- まず、初期化ベクトル (IV) を暗号化する。
その結果の一部と平文の最初の部分をXOR演算して、最初の暗号文ブロックを生成する。 - 次に、この暗号文ブロックを暗号化して、その結果と次の平文部分をXOR演算して次の暗号文ブロックを作る。
- この過程を繰り返して、全ての平文を暗号化する。
CFBモードのメリットは、エラーの伝播が限定的であることが挙げられる。
一部の暗号文が破損しても、その影響は限られた範囲にとどまる。
また、復号時に平文の長さが事前にわからなくても、リアルタイムで復号を行えるという特徴がある。
一方で、CFBモードにはいくつかの注意点もある。
例えば、初期化ベクトル (IV) の選択が重要で、同じ初期化ベクトル (IV) を再利用すると安全性が低下する可能性がある。
また、並列処理が困難であるため、大量のデータを高速に処理する必要がある場合には適していない。
CFBモードは、ネットワーク通信やストリーミングデータの暗号化等、リアルタイム性が求められる場面で特に有効である。
ただし、使用する場合は適切な初期化ベクトル (IV) の管理や必要に応じて認証機能の追加等のセキュリティ上の考慮が必要である。
OFBモード
OFBモードは、初期ベクトルを暗号化して、それをまた暗号化して、次々と乱数を生成する。
その乱数列を、XOR演算によって平文に重ね合わせて、暗号化処理を行う。
したがって、ブロック暗号をストリーム暗号のように使用する。
OFBモードの初期値は、攻撃者から見えても構わないことになっているが、同じ鍵を使用する場合、必ず、初期値は毎回違う値を使用する。
同一の初期値と鍵の組み合わせから生じる乱数列は常に同一であるため、
平文と暗号文の組が1組でも攻撃者の手に渡ってしまうと、そこから乱数列が解ってしまい、
同一の初期値と鍵で暗号化した全ての暗号文が解読されてしまう。
OFBモードは、暗号化も復号化もランダムアクセスに適していないため、どちらも先頭から順次行う必要がある。
最近、人気のある暗号化モードにカウンタモード(またはカウンタメソッド)がある。
カウンタモードは、先頭からのブロック番号を求めてその値を暗号化して、その数値を乱数として使用する。
カウンタモードであれば、安全性が若干損なう可能性があるが、暗号化も復号もランダムに行えて便利である。
CTRモード
CTRモード (Counter Mode) は、AES暗号化アルゴリズムで使用される別のブロック暗号の運用モードである。
このモードは、ブロック暗号をストリーム暗号のように機能させて、高速で並列処理が可能な暗号化を実現する。
CTRモードの基本的な動作原理は、カウンタ値を暗号化して、その結果と平文をXOR演算することで暗号文を生成するというものである。
- まず、初期値 (ノンス) とカウンタ値を組み合わせた値を暗号化する。
この暗号化された値は、キーストリームと呼ばれる。 - 次に、このキーストリームと平文をXOR演算することで暗号文を生成する。
カウンタ値は各ブロックごとに増加して、これを繰り返して全ての平文を暗号化する。
CTRモードの大きな特徴の1つは、暗号化と復号が同じ操作で行えることである。
つまり、暗号化アルゴリズムのみを実装するだけでよく、ハードウェア実装が容易になる。
また、各ブロックの暗号化が他のブロックに依存しないため、並列処理が可能であり、高速な暗号化・復号を実現できる。
さらに、CTRモードはランダムアクセスが可能である。
つまり、暗号文の特定の部分だけを復号することができる。
これは大きなファイルの一部だけを復号する必要がある場合等に有効である。
ただし、CTRモードにも注意点がある。
例えば、同じ鍵とノンスの組み合わせを再利用する場合、セキュリティが著しく低下する可能性がある。
また、暗号文の改竄が容易であるため、認証機能を別途追加する必要がある。
CTRモードは、その高速性と並列処理の特性から、大量のデータを扱うディスク暗号化やネットワークプロトコルでの利用に適している。
例えば、IPsecやSSL / TLS等のセキュリティプロトコルで使用されている。
CTRモードを安全に使用するためには、適切な鍵管理とノンスの生成が重要である。
また、必要に応じて、GCM (Galois / Counter Mode) モードのような認証付き暗号モードを使用することにより、機密性と完全性を同時に確保することができる。
GCMモード
GCM (Galois / Counter Mode) は、AESで使用される高速で安全な認証付き暗号化モードの1つである。
このモードは、データの機密性と完全性を同時に提供する効率的な方法として広く採用されている。
GCMモードの主な特徴は、暗号化と認証を同時に行えることである。
これにより、従来の方式に比べてパフォーマンスが向上して、実装も簡素化される。
GCMの動作原理を以下に示す。
- まず、CTRモードでデータを暗号化する。
- その後、暗号文とAAD (追加認証データ) を使用して認証タグを生成する。
この過程でガロアフィールド上の乗算が利用されるため、ガロアの名前が付いている。
セキュリティの観点から、GCMモードは非常に堅牢とされている。
ただし、使用する場合は1つのキーで大量のデータを暗号化しないこと、初期化ベクトル (IV) の再利用を避けることが重要である。
GCMのメリットとして、並列処理が可能であることが挙げられる。
これにより、ハードウェア実装での高速化が容易になる。
また、パディングが不要なため、データの長さが変わらないという特徴もある。
GCMモードにも課題はあり、例えば、実装が複雑になる可能性があることや一部の実装では副チャネル攻撃に対して脆弱性を持つ可能性があること等が挙げられる。
GCMモードは多くの暗号プロトコルで採用されている。
例えば、TLS 1.2以降のバージョンでサポートされており、安全な通信に広く使用されている。
初期化ベクトル
カウンタモード (CTR)
CTRモードでは、カウンタ値を初期化ベクトルとして使用する。
各ブロックごとにカウンタをインクリメントすることにより、一意の値を生成する。
GCMモード
GCMモード等では、ナンス (使い捨ての数値) とカウンタを組み合わせて初期化ベクトルを生成する。
初期化ベクトル生成時の注意点
- 予測可能性
- 自動生成されたIV (初期化ベクトル) が予測可能であってはいけない。
- 例えば、単純なインクリメント方式は避けるべきである。
- 一意性
- 同じキーで同じIV (初期化ベクトル) を再利用しないことが重要である。
- 特にストリーム暗号的なモード (CTRモード, GCMモード) では致命的な脆弱性につながる。
- ランダム性
- 可能な限り、暗号学的に安全な乱数生成器を使用してIV (初期化ベクトル) を生成すべきである。
- 通信
- IV (初期化ベクトル) は、一般的に、暗号文と一緒に送信する必要がある。
- 多くの場合、暗号文の先頭に付加される。
- 長さ
- IV (初期化ベクトル) の長さは、使用する暗号化モードに応じて適切に設定する必要がある。
- 多くの場合、ブロックサイズと同じである。
自動生成されたIV (初期化ベクトル) を使用することにより、同じ平文でも毎回異なる暗号文が生成されて、セキュリティが向上する。
QCrypto
サンプルコード : CBCモード
以下の例では、QCrypto
クラスを使用して、CBCモードでの暗号化と復号を行っている。
- キー管理
- setKeyメソッドでパスワードからキーを生成する。
- キーが設定されていない場合は、暗号化・復号操作は失敗する。
- ただし、以下の例ではパスワードからキーを生成しているが、実務では、より安全なキー管理方法を使用すべきである。
- IV (初期化ベクトル) の自動生成
- 暗号化時において、自動的に新しいランダムなIV (初期化ベクトル) を生成して、IV (初期化ベクトル) を暗号文の先頭に付加して送信する。
- 復号時において、暗号文からIV (初期化ベクトル) を自動的に取り出して使用する。
- パディング
- PKCS7パディングを使用しており、復号時にパディングの妥当性を確認する。
# CMakeLists.txtファイル
# ...略
# Pkg-configの使用
find_package(PkgConfig REQUIRED)
# Qtライブラリの検索
find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core REQUIRED)
# OpenSSLライブラリの検索
find_package(OpenSSL REQUIRED)
# ...略
# Qtライブラリのリンク
target_link_libraries(${PROJECT_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::Core)
# OpenSSLライブラリのリンク
target_link_libraries(${PROJECT_NAME} PRIVATE OpenSSL::SSL OpenSSL::Crypto)
# OpenSSLのインクルードディレクトリ
target_include_directories(${PROJECT_NAME} PRIVATE ${OPENSSL_INCLUDE_DIR})
# C++コンパイラフラグの設定
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -pedantic)
elseif(MSVC)
target_compile_options(${PROJECT_NAME} PRIVATE /W4)
endif()
// 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);
}
// main.cppファイル
#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 (初期化ベクトル) を取り出して使用する。
- パスワードからキーを生成しているが、実務ではより安全なキー管理方法を使用すべきである。
- エラーハンドリングにおいて、入出力の検証を追加することを推奨する。
パスワード / キーの管理
パスワード / キーの管理は非常に重要なセキュリティ上の問題である。
ハードコーディングされたパスワードは避けるべきであり、より安全な方法でパスワードを取得・管理する必要がある。
以下に、いくつかの一般的なアプローチと推奨事項を示す。
ユーザ入力
最も簡単な方法は、ユーザに直接パスワードを入力してもらうことである。
QString getPasswordFromUser()
{
QInputDialog dialog;
dialog.setInputMode(QInputDialog::TextInput);
dialog.setTextEchoMode(QLineEdit::Password);
dialog.setWindowTitle("パスワードの入力");
dialog.setLabelText("暗号化パスワードを入力してください: ");
if (dialog.exec() == QDialog::Accepted) {
return dialog.textValue();
}
return QString();
}
// 使用例
QString password = getPasswordFromUser();
if (!password.isEmpty()) {
aes.setKey(password);
}
環境変数の使用
セキュリティ上のリスクはあるが、開発環境やテスト環境では環境変数を使用することがある。
QString getPasswordFromEnv()
{
return qgetenv("MY_APP_ENCRYPTION_KEY");
}
暗号化された設定ファイル
暗号化された設定ファイルにパスワードを保存する方法もある。
ただし、この方法を使用する場合は、設定ファイル自体を安全に保護する必要がある。
QString getPasswordFromConfig()
{
QSettings settings("MyCompany", "MyApp");
return settings.value("EncryptionKey").toString();
}
キーストア / セキュアエンクレーブ
OSのセキュアなキーストアを使用する方法である。
これは最も安全な方法の1つであるが、実装が複雑になる可能性がある。
以下に示すAPIを使用する場合は、プラットフォーム固有のコードが必要になる。
- Windows
- Data Protection API (DPAPI)
- MacOS
- Keychain
- Linux
- Secret Service API
ハードウェアセキュリティモジュール (HSM)
高度なセキュリティが必要な場合、HSMを使用してキーを管理することができる。
キーの派生
ユーザのパスワードから安全にキーを派生させる方法もある。
これには、PBKDF2やscrypt等のキー派生関数を使用する。
QByteArray deriveKey(const QString &password, const QByteArray &salt)
{
// OpenSSLを使用してPBKDF2でキーを派生させる例
QByteArray key(32, 0); // 256ビットキー
PKCS5_PBKDF2_HMAC(password.toUtf8().constData(), password.length(),
reinterpret_cast<const unsigned char*>(salt.constData()),
salt.length(), 10000, EVP_sha256(),
key.size(), reinterpret_cast<unsigned char*>(key.data()));
return key;
}
推奨事項
- パスワードを平文で保存しないこと。
- 可能な限り、ユーザに直接パスワードを入力してもらう方法を選択すること。
- パスワードの強度要件を設定して、ユーザに強力なパスワードの使用を促す。
- パスワード / キーの転送時は、安全な通信チャネル (例: HTTPS) を使用すること。
- 必要に応じて、多要素認証を実装することを検討する。
- 定期的なパスワード変更を促すポリシーを実装することを検討する。
実務では、これらの方法を組み合わせて使用することが一般的である。
例えば、ユーザが入力したパスワードからキーを派生させて、派生したキーをセキュアなキーストアに保存するという方法がある。
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
サンプルコード : CBCモード
以下の例では、CBCモードを使用して、暗号化および復号を行っている。
使用する鍵はINIファイルでを保存して、アプリケーション再起動時にも使用できるようにしている。
また、メモリ使用量を考慮して、ストリーミング暗号化および復号を行っている。
以下の例では、16バイト (128ビット) の鍵を使用しているが、AESは192ビットや256ビットのキーもサポートしている。
暗号化されたデータを保存または送信する場合は、初期化ベクトル (IV) も一緒に保存または送信する必要がある。
// SecureAESCrypto.hファイル
#include <QCoreApplication>
#include <QString>
#include <QByteArray>
#include <QSettings>
#include <QFile>
#include <QDebug>
#include <cryptopp/aes.h>
#include <cryptopp/modes.h>
#include <cryptopp/filters.h>
#include <cryptopp/osrng.h>
#include <cryptopp/hex.h>
class SecureAESCrypto {
public:
// 安全な鍵の生成
static QByteArray generateKey()
{
QByteArray key;
key.resize(CryptoPP::AES::DEFAULT_KEYLENGTH);
CryptoPP::AutoSeededRandomPool prng;
prng.GenerateBlock(reinterpret_cast<byte*>(key.data()), key.size());
return key;
}
// 安全な初期化ベクトル (IV) の生成
static QByteArray generateIV()
{
QByteArray iv;
iv.resize(CryptoPP::AES::BLOCKSIZE);
CryptoPP::AutoSeededRandomPool prng;
prng.GenerateBlock(reinterpret_cast<byte*>(iv.data()), iv.size());
return iv;
}
static bool encryptFile(const QString &inputFilename, const QString &outputFilename, const QByteArray &key, QByteArray &iv) {
try {
CryptoPP::CBC_Mode<CryptoPP::AES>::Encryption encryptor;
encryptor.SetKeyWithIV(reinterpret_cast<const byte*>(key.constData()), key.size(), reinterpret_cast<const byte*>(iv.constData()));
CryptoPP::FileSource(inputFilename.toStdString().c_str(), true,
new CryptoPP::StreamTransformationFilter(encryptor, new CryptoPP::FileSink(outputFilename.toStdString().c_str())));
return true;
}
catch (const CryptoPP::Exception& e) {
qCritical() << "File encryption error:" << e.what();
return false;
}
}
static bool decryptFile(const QString &inputFilename, const QString &outputFilename, const QByteArray &key, const QByteArray &iv)
{
try {
CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption decryptor;
decryptor.SetKeyWithIV(reinterpret_cast<const byte*>(key.constData()), key.size(), reinterpret_cast<const byte*>(iv.constData()));
CryptoPP::FileSource(inputFilename.toStdString().c_str(), true,
new CryptoPP::StreamTransformationFilter(decryptor, new CryptoPP::FileSink(outputFilename.toStdString().c_str())));
return true;
}
catch (const CryptoPP::Exception& e) {
qCritical() << "File decryption error:" << e.what();
return false;
}
}
// 平文を暗号化
static QByteArray encrypt(const QByteArray &plaintext, const QByteArray &key, QByteArray &iv)
{
try {
CryptoPP::CBC_Mode<CryptoPP::AES>::Encryption encryptor;
encryptor.SetKeyWithIV(reinterpret_cast<const byte*>(key.constData()), key.size(), reinterpret_cast<const byte*>(iv.constData()));
QByteArray ciphertext;
CryptoPP::StringSource(reinterpret_cast<const byte*>(plaintext.constData()), plaintext.size(), true,
new CryptoPP::StreamTransformationFilter(encryptor, new CryptoPP::StringSink(ciphertext)));
return ciphertext;
}
catch (const CryptoPP::Exception& e) {
qCritical() << "Encryption error:" << e.what();
return QByteArray();
}
}
// 暗号文を復号
static QByteArray decrypt(const QByteArray &ciphertext, const QByteArray &key, const QByteArray &iv)
{
try {
CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption decryptor;
decryptor.SetKeyWithIV(reinterpret_cast<const byte*>(key.constData()), key.size(), reinterpret_cast<const byte*>(iv.constData()));
QByteArray plaintext;
CryptoPP::StringSource(reinterpret_cast<const byte*>(ciphertext.constData()), ciphertext.size(), true,
new CryptoPP::StreamTransformationFilter(decryptor, new CryptoPP::StringSink(plaintext)));
return plaintext;
}
catch (const CryptoPP::Exception& e) {
qCritical() << "Decryption error:" << e.what();
return QByteArray();
}
}
// 暗号文と初期化ベクトル (IV) をファイルに保存
static bool saveEncryptedData(const QString &filename, const QByteArray &data, const QByteArray &iv)
{
QFile file(filename);
if (!file.open(QIODevice::WriteOnly)) {
qCritical() << "Unable to open file for writing:" << filename;
return false;
}
QDataStream out(&file);
out << iv << data;
file.close();
return true;
}
// ファイルから暗号文と初期化ベクトル (IV) を読み込む
static bool loadEncryptedData(const QString &filename, QByteArray &data, QByteArray &iv)
{
QFile file(filename);
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Unable to open file for reading:" << filename;
return false;
}
QDataStream in(&file);
in >> iv >> data;
file.close();
return true;
}
static bool saveIV(const QString &filename, const QByteArray &iv)
{
QFile file(filename);
if (!file.open(QIODevice::WriteOnly)) {
qCritical() << "Unable to open file for writing IV:" << filename;
return false;
}
QDataStream out(&file);
out << iv;
file.close();
return true;
}
static bool loadIV(const QString &filename, QByteArray &iv)
{
QFile file(filename);
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Unable to open file for reading IV:" << filename;
return false;
}
QDataStream in(&file);
in >> iv;
file.close();
return true;
}
};
// main.cppファイル
#include "SecureAESCrypto.h"
QByteArray getOrCreateKey()
{
QSettings settings("MyCompany", "SecureAESApp");
if (settings.contains("encryption/key")) {
return QByteArray::fromHex(settings.value("encryption/key").toString().toLatin1());
}
else {
QByteArray key = SecureAESCrypto::generateKey();
settings.setValue("encryption/key", QString(key.toHex()));
return key;
}
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// キーの取得または生成
QByteArray key = getOrCreateKey();
qDebug() << "Encryption key:" << key.toHex();
// 入力ファイルと出力ファイルの設定
QString inputFilename = "input.txt"; // 暗号化するファイル
QString encryptedFilename = "encrypted.bin"; // 暗号化されたファイル
QString decryptedFilename = "decrypted.txt"; // 復号されたファイル
QString ivFilename = "iv.bin"; // 初期化ベクトル (IV) ファイル
// 平文を暗号化
QByteArray iv = SecureAESCrypto::generateIV();
if (SecureAESCrypto::encryptFile(inputFilename, encryptedFilename, key, iv)) {
qDebug() << "File encrypted successfully.";
// 初期化ベクトル (IV) の保存
if (SecureAESCrypto::saveIV(ivFilename, iv)) {
qDebug() << "IV saved successfully.";
// 初期化ベクトル (IV) の読み込み
QByteArray loadedIV;
if (SecureAESCrypto::loadIV(ivFilename, loadedIV)) {
qDebug() << "IV loaded successfully.";
// 復号
if (SecureAESCrypto::decryptFile(encryptedFilename, decryptedFilename, key, loadedIV)) {
qDebug() << "File decrypted successfully.";
}
else {
qDebug() << "File decryption failed.";
}
}
else {
qDebug() << "Failed to load IV.";
}
}
else {
qDebug() << "Failed to save IV.";
}
}
else {
qDebug() << "File encryption failed.";
}
return a.exec();
}
※注意
鍵の保存において、実務ではより安全な方法 (例: OSのキーチェーンやセキュアな暗号化ストレージ) を使用することを推奨する。
また、ユーザ認証やアクセス制御等の追加のセキュリティ層を実装することを推奨する。
ファイルの暗号化や大量のデータの暗号化を行う場合は、メモリ使用量を考慮してストリーミング暗号化を行う。
サンプルコード : CFBモード
CFBモードは、ブロック暗号をストリーム暗号のように使用できるモードであり、データを1[byte]ずつ暗号化できる特徴がある。
CFBモードの特徴を以下に示す。
- ストリーム暗号のように動作するため、データを1バイトずつ暗号化できる。
- 暗号文の一部が破損しても、その影響は限定的である。 (エラーの伝播が少ない)
- 並列化が難しいため、大量のデータを高速に処理する必要がある場合は他のモードを検討する必要がある。
以下の例では、CFBモードを使用して、暗号化および復号を行っている。
ECBモードやCBCモードとは異なり、データ長をAESのブロックサイズの倍数にする必要は無い。
// SecureAESCrypto.hファイル
#include <QCoreApplication>
#include <QString>
#include <QByteArray>
#include <QSettings>
#include <QFile>
#include <QDebug>
#include <cryptopp/aes.h>
#include <cryptopp/modes.h>
#include <cryptopp/filters.h>
#include <cryptopp/osrng.h>
#include <cryptopp/hex.h>
class SecureAESCrypto {
public:
// 安全な鍵の生成
static QByteArray generateKey()
{
QByteArray key;
key.resize(CryptoPP::AES::DEFAULT_KEYLENGTH);
CryptoPP::AutoSeededRandomPool prng;
prng.GenerateBlock(reinterpret_cast<byte*>(key.data()), key.size());
return key;
}
// 安全な初期化ベクトル (IV) の生成
static QByteArray generateIV()
{
QByteArray iv;
iv.resize(CryptoPP::AES::BLOCKSIZE);
CryptoPP::AutoSeededRandomPool prng;
prng.GenerateBlock(reinterpret_cast<byte*>(iv.data()), iv.size());
return iv;
}
// 暗号化
static bool encryptFile(const QString &inputFilename, const QString &outputFilename, const QByteArray &key, QByteArray &iv)
{
try {
CryptoPP::CFB_Mode<CryptoPP::AES>::Encryption encryptor;
encryptor.SetKeyWithIV(reinterpret_cast<const byte*>(key.constData()), key.size(), reinterpret_cast<const byte*>(iv.constData()));
CryptoPP::FileSource(inputFilename.toStdString().c_str(), true,
new CryptoPP::StreamTransformationFilter(encryptor, new CryptoPP::FileSink(outputFilename.toStdString().c_str())));
return true;
}
catch (const CryptoPP::Exception& e) {
qCritical() << "File encryption error:" << e.what();
return false;
}
}
// 復号
static bool decryptFile(const QString &inputFilename, const QString &outputFilename, const QByteArray &key, const QByteArray &iv)
{
try {
CryptoPP::CFB_Mode<CryptoPP::AES>::Decryption decryptor;
decryptor.SetKeyWithIV(reinterpret_cast<const byte*>(key.constData()), key.size(), reinterpret_cast<const byte*>(iv.constData()));
CryptoPP::FileSource(inputFilename.toStdString().c_str(), true,
new CryptoPP::StreamTransformationFilter(decryptor, new CryptoPP::FileSink(outputFilename.toStdString().c_str())));
return true;
}
catch (const CryptoPP::Exception& e) {
qCritical() << "File decryption error:" << e.what();
return false;
}
}
// 初期化ベクトル (IV) の保存
static bool saveIV(const QString &filename, const QByteArray &iv)
{
QFile file(filename);
if (!file.open(QIODevice::WriteOnly)) {
qCritical() << "Unable to open file for writing IV:" << filename;
return false;
}
QDataStream out(&file);
out << iv;
file.close();
return true;
}
// 初期化ベクトル (IV) の読み込み
static bool loadIV(const QString &filename, QByteArray &iv)
{
QFile file(filename);
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Unable to open file for reading IV:" << filename;
return false;
}
QDataStream in(&file);
in >> iv;
file.close();
return true;
}
};
// main.cppファイル
#include "SecureAESCrypto.h"
QByteArray getOrCreateKey()
{
QSettings settings("MyCompany", "SecureAESApp");
if (settings.contains("encryption/key")) {
return QByteArray::fromHex(settings.value("encryption/key").toString().toLatin1());
}
else {
QByteArray key = SecureAESCrypto::generateKey();
settings.setValue("encryption/key", QString(key.toHex()));
return key;
}
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 鍵の取得または生成
QByteArray key = getOrCreateKey();
qDebug() << "Encryption key:" << key.toHex();
// 入力ファイルと出力ファイルの設定
QString inputFilename = "input.txt";
QString encryptedFilename = "encrypted.bin";
QString decryptedFilename = "decrypted.txt";
QString ivFilename = "iv.bin";
// 暗号化
QByteArray iv = SecureAESCrypto::generateIV();
if (SecureAESCrypto::encryptFile(inputFilename, encryptedFilename, key, iv)) {
qDebug() << "File encrypted successfully.";
// 初期化ベクトル (IV) の保存
if (SecureAESCrypto::saveIV(ivFilename, iv)) {
qDebug() << "IV saved successfully.";
// 初期化ベクトル (IV) の読み込み
QByteArray loadedIV;
if (SecureAESCrypto::loadIV(ivFilename, loadedIV)) {
qDebug() << "IV loaded successfully.";
// 復号
if (SecureAESCrypto::decryptFile(encryptedFilename, decryptedFilename, key, loadedIV)) {
qDebug() << "File decrypted successfully.";
}
else {
qDebug() << "File decryption failed.";
}
}
else {
qDebug() << "Failed to load IV.";
}
}
else {
qDebug() << "Failed to save IV.";
}
}
else {
qDebug() << "File encryption failed.";
}
return a.exec();
}
※注意
CFBモードは、CBCモードと比較してエラーの伝播が少ないというメリットがあるが、並列処理には向いていない。
大きなファイルを暗号化・復号する場合は、進捗状況を表示する機能を追加することを推奨する。
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暗号化クラスである。
全ての鍵長 (128/192/256ビット) をサポートしている。
また、全ての鍵長に対応したECB、CBC、CFB、OFBモードおよび部分的なAES-NIをサポートしている。
デフォルトのパディング方法はISOであるが、ZERO、PKCS7もサポートしている。
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;
}