Qtの基礎 - パケット
概要
AESを使用した暗号化
以下の例では、QAESEncryption
クラスを使用して、AES(CBCモード)暗号化を指定して、パケットデータを暗号化している。
また、パケット長を計算して、ネットワークバイトオーダーに変換している。
最後に、QTcpSocket::write
メソッドを使用して、パケットを送信している。
#include <QtNetwork>
#include <QDataStream>
#include <QCryptographicHash>
#include <QIODevice>
// パケットデータをAES暗号化
QByteArray encryptPacket(const QByteArray &data, const QByteArray &key, const QByteArray &iv)
{
// AES(CBCモード)の指定
QAESEncryption encryption(QAESEncryption::AES_128, QAESEncryption::CBC);
encryption.setKey(key);
encryption.setIv(iv);
// データを暗号化
QByteArray encrypted = encryption.encode(data);
return encrypted;
}
// パケットの送信
void sendPacket(QTcpSocket *socket, const QByteArray &data, const QByteArray &key, const QByteArray &iv)
{
// パケットデータをAES暗号化
QByteArray encryptedData = encryptPacket(data, key, iv);
// パケット長の計算
quint16 packetLength = encryptedData.size();
// パケット長をネットワークバイトオーダーへ変換
QByteArray packetLengthBytes;
QDataStream lengthStream(&packetLengthBytes, QIODevice::WriteOnly);
lengthStream.setByteOrder(QDataStream::BigEndian);
lengthStream << packetLength;
// パケットの送信
socket->write(packetLengthBytes);
socket->write(encryptedData);
}
int main()
{
QTcpSocket socket;
// 接続処理
QByteArray key = "your_key"; // 鍵
QByteArray iv = "your_iv"; // 初期化ベクトル
QByteArray packetData = "your_data"; // 送信するパケット
sendPacket(&socket, packetData, key, iv);
}
以下の例では、QAESEncryption
クラスを使用して、AES(CBCモード)復号を指定して、パケットを復号している。
まず、パケット長を受信して、次に、パケットを受信してAES復号する。
#include <QtNetwork>
#include <QDataStream>
#include <QCryptographicHash>
#include <QIODevice>
// パケットをAES復号
QByteArray decryptPacket(const QByteArray &data, const QByteArray &key, const QByteArray &iv)
{
// AES(CBCモード)復号を指定
QAESEncryption decryption(QAESEncryption::AES_128, QAESEncryption::CBC);
decryption.setKey(key);
decryption.setIv(iv);
// データを復号
QByteArray decrypted = decryption.decode(data);
return decrypted;
}
// パケットの受信
QByteArray receivePacket(QTcpSocket *socket, const QByteArray &key, const QByteArray &iv)
{
QByteArray packetData;
qint64 packetLength = -1;
// パケット長の受信
while(socket->bytesAvailable() < (qint64)sizeof(packetLength))
{
if(!socket->waitForReadyRead())
{
return QByteArray();
}
}
QDataStream lengthStream(socket);
lengthStream >> packetLength;
// パケットの受信
while (socket->bytesAvailable() < packetLength)
{
if(!socket->waitForReadyRead())
{
return QByteArray();
}
}
packetData = socket->read(packetLength);
// パケットのAES復号
QByteArray decryptedData = decryptPacket(packetData, key, iv);
return decryptedData;
}
int main()
{
QTcpSocket socket;
// 接続処理
QByteArray key = "your_key"; // 鍵
QByteArray iv = "your_iv"; // 初期化ベクトル
QByteArray receivedPacket = receivePacket(&socket, key, iv);
}
SSLを使用した暗号化
暗号化
QSslSocket
クラスは、暗号化されたデータの送信に使用できる安全で暗号化されたTCP接続を確立する。
クライアントモードとサーバーモードの両方で動作し、TLS 1.3を含む最新のTLSプロトコルをサポートしている。
デフォルトでは、QSslSocket
クラスは、安全であると考えられているTLSプロトコル(QSsl::SecureProtocols
)のみを使用するが、
ハンドシェイクが始まる前に行う限り、setProtocol
メソッドを呼び出してTLSプロトコルを変更することが可能である。
SSL暗号化は、ソケットがConnectedState
になった後、既存のTCPストリームの上で動作する。
QSslSocket
クラスを使用して安全な接続を確立するには、以下に示す2つの方法がある。
- 即座にSSLハンドシェイクを行う方法
- 非暗号化モードで接続が確立された後、遅延SSLハンドシェイクを行う方法
QSslSocket
クラスの最も一般的な使用方法は、QSslSocket
クラスのインスタンスを生成して、connectToHostEncrypted
メソッドを呼び出して、安全な接続を開始する方法である。
connectToHostEncrypted
メソッドは、接続が確立されると即座にSSLハンドシェイクを開始する。
auto pSocket = std::make_unique<QSslSocket>(this);
connect(pSocket, &QSslSocket::encrypted, this, &CSampleClass::ready);
pSocket->connectToHostEncrypted("example.com", 993);
プレーンなQTcpSocket
クラスと同様、QSslSocket
クラスはHostLookupState
-> ConnectingState
になり、接続が成功するとConnectedState
になる。
その後、自動的にハンドシェイクが開始されて、成功するとソケットが暗号化状態になり、使用可能な状態になったことを示すencrypted
シグナルが送信される。
この時、connectToHostEncrypted
メソッドのリターン直後(encrypted
シグナルが送信される前)に、ソケットにデータを書き込むことができることに注意する。
データは、encrypted
シグナルが送信されるまで、QSslSocket
クラスにキューイングされる。<br<
既存の接続を保護するために遅延SSLハンドシェイクを使用する例として、SSLサーバが着信接続を保護する場合がある。
以下の例のように、QTcpServer
クラスを継承して、SslServerクラスを定義したとする。
まず、QSslSocket
クラスのインスタンスを生成した後、setSocketDescriptor
メソッドを呼んで、新しいソケットのディスクリプタを渡された既存のものに設定する。
そして、QTcpServer::incomingConnection
メソッドをオーバーライドして、そのメソッド内でstartServerEncryption
メソッドを呼び出すことにより、SSLハンドシェイクを開始する。
void SslServer::incomingConnection(qintptr socketDescriptor)
{
auto pServerSocket = std::make_unique<QSslSocket>();
if(pServerSocket->setSocketDescriptor(socketDescriptor))
{
addPendingConnection(pServerSocket);
connect(pServerSocket, &QSslSocket::encrypted, this, &SslServer::ready);
pServerSocket->startServerEncryption();
}
}
エラーが発生した場合、QSslSocket
クラスはsslErrors
シグナルを送信する。
この時、エラーを無視するためのアクションが取られない場合は、接続は切断される。
エラーが発生しても接続を継続する場合は、エラー発生後にこのスロット内、または、QSslSocket
クラスのインスタンス生成後、接続が試行される前であればignoreSslErrors
メソッドを呼び出すことができる。
これは、QSslSocket
クラスが相手のIDを確立する時に遭遇したエラーを無視することができる。
安全な接続は成功したハンドシェイクで確立されるべきであるため、SSLハンドシェイク中のエラーを無視することは慎重になるべきである。
1度暗号化されると、QSslSocket
クラスは通常のQTcpSocket
クラスとして使用される。
readyRead
シグナルが送信される時、read
メソッド、canReadLine
メソッド、readLine
メソッド、getChar
メソッドを使用してQSslSocket
クラスの内部バッファから復号したデータを読み、
write
メソッド、putChar
メソッドを使用して相手にデータを書き戻すことができる。
QSslSocket
クラスは、書き込まれたデータを自動的に暗号化して、データがピアに書き込まれるとencryptedBytesWritten
シグナルを送信する。
便利なことに、QSslSocket
クラスは、QTcpSocket
クラスのブロッキングメソッドである以下に示すメソッドが存在する。
waitForConnected
メソッドwaitForReadyRead
メソッドwaitForBytesWritten
メソッドwaitForDisconnected
メソッド
また、暗号化された接続が確立されるまで呼び出し側のスレッドをブロックするwaitForEncrypted
メソッドも提供されている。
QSslSocket socket;
socket.connectToHostEncrypted("http.example.com", 443);
if(!socket.waitForEncrypted())
{
qDebug() << socket.errorString();
return false;
}
socket.write("GET / HTTP/1.0\r\n\r\n");
while(socket.waitForReadyRead())
{
qDebug() << socket.readAll().data();
}
QSslSocket
クラスは、暗号、秘密鍵、ローカル証明書、ピア証明書、認証局(CA)証明書を扱うための広範で使い勝手の良いAPIを提供する。
また、ハンドシェイクフェーズで発生したエラーを処理するためのAPIも提供している。
また、以下の機能をカスタマイズすることも可能である。
QSslConfiguration::setCiphers
メソッド、および、QSslConfiguration::setDefaultCiphers
メソッドにより、ハンドシェイク前にソケットの暗号化スイートをカスタマイズすることができる。- ソケットのローカル証明書と秘密鍵は、ハンドシェイクフェーズの前に
setLocalCertificate
メソッドとsetPrivateKey
メソッドを使用して、カスタマイズすることができる。 QSslConfiguration::addCaCertificate
メソッド、QSslConfiguration::addCaCertificates
メソッドを使用することにより、CA証明書データベースを拡張し、カスタマイズすることができる。
SSLハンドシェイク中にSSLソケットで使用されるデフォルトCA証明書のリストを拡張するには、デフォルトの設定を更新する必要がある。
QList<QSslCertificate> certificates = getCertificates();
QSslConfiguration configuration = QSslConfiguration::defaultConfiguration();
configuration.addCaCertificates(certificates);
QSslConfiguration::setDefaultConfiguration(configuration);
自己署名証明書を使用する場合
自己署名証明書と秘密鍵を作成する。
openssl req -x509 -sha256 -newkey rsa:3072 \ -subj "/C=JP/ST=Tokyo/L=Tokyo City/O=Company Name/OU=Department/CN=localhost" \ -keyout sshconfig.key -out sshconfig.pem -days 3650 -nodes
自己署名証明書をインストールする場合
自己署名証明書を信頼済みのルート証明書としてインストールする。
まず、自己署名証明書と秘密鍵を作成する。
openssl req -x509 -sha256 -newkey rsa:3072 \ -subj "/C=JP/ST=Tokyo/L=Tokyo City/O=Company Name/OU=Department/CN=localhost" \ -keyout sshconfig.key -out sshconfig.pem -days 3650 -nodes
次に、自己署名証明書を、以下に示すディレクトリに配置する。
- /etc/pki/trust/anchors
- 当ディレクトリに配置する場合は、サブディレクトリを作成してもよい。
- /usr/share/pki/trust/anchors
- 当ディレクトリに配置する場合は、サブディレクトリは作成しない。
sudo cp sshconfig.pem /etc/pki/trust/anchors # または sudo cp sshconfig.pem /usr/share/pki/trust/anchors
最後に、証明書を信頼済みルート証明書に追加する。
sudo update-ca-certificates
自己署名証明書をアンインストールする。
sudo rm /etc/pki/trust/anchors/sshconfig.pem sudo update-ca-certificates
サンプルコード
以下の例では、OpenSSLライブラリとQTcpSocket
クラスを使用して、パケットを暗号化して送信している。
#include <QTcpSocket>
#include <QSslSocket>
#include <QSslKey>
#include <QSslCertificate>
#include <QFile>
// 暗号化
void encryptPacket(QByteArray &packet)
{
// OpenSSLライブラリの初期化
SSL_load_error_strings();
SSL_library_init();
// 公開鍵と秘密鍵を読み込む
QSslKey privateKey("server.key", QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey);
QSslCertificate publicKey("server.crt", QSsl::Pem);
// QSslSocketオブジェクトの生成
QSslSocket socket;
socket.setPrivateKey(privateKey);
socket.setLocalCertificate(publicKey);
socket.connectToHostEncrypted("example.com", 443);
// パケットの暗号化
packet = socket.encrypt(packet);
}
int main()
{
// パケットの生成
QByteArray packet = "Hello, world!";
// パケットの暗号化
encryptPacket(packet);
// 暗号化されたパケットの送信
QTcpSocket socket;
socket.connectToHost("example.com", 1234); // ホスト名とTCPポート番号の指定
socket.write(packet);
// 送信が完了するまで待機
socket.waitForBytesWritten();
return 0;
}
以下の例では、暗号化されたパケットを送信するために、TCPソケットとパケットデータを引数として受け取り、OpenSSLライブラリを使用してSSLセッションを確立して、パケットを暗号化して送信する。
SSLセッションは、QSslSocket
クラスを使用して確立される。
この時、事前に、サーバの証明書、クライアントの秘密鍵、サーバの公開鍵をファイルから読み込む必要がある。
#include <QTcpSocket>
#include <QSslSocket>
#include <QSslKey>
#include <QSslCertificate>
#include <QFile>
void sendEncryptedPacket(QTcpSocket *socket, const QByteArray &packet)
{
// OpenSSLライブラリの初期化
SSL_load_error_strings();
SSL_library_init();
// SSLコンテキストを生成
SSL_CTX *ctx = SSL_CTX_new(TLSv1_2_client_method());
// サーバの証明書を読み込む
QFile certFile("server.crt");
certFile.open(QIODevice::ReadOnly);
QSslCertificate cert(certFile.readAll());
certFile.close();
SSL_CTX_use_certificate(ctx, cert.handle());
// クライアントの秘密鍵を読み込む
QFile keyFile("client.key");
keyFile.open(QIODevice::ReadOnly);
QSslKey key(keyFile.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, "my passphrase");
keyFile.close();
SSL_CTX_use_PrivateKey(ctx, key.handle());
// サーバの公開鍵を読み込む
QFile pubkeyFile("server.pub");
pubkeyFile.open(QIODevice::ReadOnly);
QSslCertificate pubkey(pubkeyFile.readAll());
pubkeyFile.close();
SSL_CTX_add_extra_chain_cert(ctx, pubkey.handle());
// SSLセッションの確立
QSslSocket sslSocket;
sslSocket.setSocketDescriptor(socket->socketDescriptor());
sslSocket.setProtocol(QSsl::TlsV1_2);
sslSocket.setLocalCertificate(cert);
sslSocket.setPrivateKey(key);
sslSocket.addCaCertificate(pubkey);
sslSocket.startClientEncryption();
// パケットの暗号化
QByteArray encryptedPacket = sslSocket.encrypt(packet);
// 暗号化されたパケットの送信
socket->write(encryptedPacket);
socket->flush();
socket->waitForBytesWritten();
// SSLセッションの終了
sslSocket.close();
}