#include <QtDebug>
+#ifndef ANIDBUDPCLIENT_NO_ENCRYPT
+# include <QCryptographicHash>
+# include <QtCrypto>
+#endif
+
namespace AniDBUdpClient {
const QByteArray Client::clientName = CLIENT_NAME;
#endif
authReply = 0;
uptimeReply = 0;
+ encryptReply = 0;
m_idlePolicy = DoNothingIdlePolicy;
+ m_enableEncryption = false;
disconnecting = false;
authenticatingStarted = false;
+ usingEncryption = false;
commandsTimedOut = 0;
socket = new QUdpSocket(this);
connectingState = new QState;
connectedState = new QState;
authenticatingState = new QState(connectedState);
+ encryptionState = new QState(connectedState);
idleState = new QState(connectedState);
idleTimeoutState = new QState(connectedState);
sendState = new QState(connectedState);
authenticatingState->addTransition(this, SIGNAL(startSending()), sendState);
authenticatingState->addTransition(this, SIGNAL(authenticated()), sendState);
+ encryptionState->addTransition(this, SIGNAL(startSending()), sendState);
+ encryptionState->addTransition(this, SIGNAL(encryptionEstablished()), sendState);
+
sendState->addTransition(this, SIGNAL(queueEmpty()), idleState);
sendState->addTransition(this, SIGNAL(commandSent()), waitState);
sendState->addTransition(this, SIGNAL(startAuthentication()), authenticatingState);
+ sendState->addTransition(this, SIGNAL(startEncryption()), encryptionState);
waitState->addTransition(commandTimer, SIGNAL(timeout()), sendState);
QObject::connect(connectingState, SIGNAL(entered()), this, SLOT(enterConnectingState()));
QObject::connect(connectedState, SIGNAL(entered()), this, SLOT(enterConnectedState()));
QObject::connect(authenticatingState, SIGNAL(entered()), this, SLOT(enterAuthenticatingState()));
+ QObject::connect(encryptionState, SIGNAL(entered()), this, SLOT(enterEncryptionState()));
QObject::connect(sendState, SIGNAL(entered()), this, SLOT(enterSendState()));
QObject::connect(waitState, SIGNAL(entered()), this, SLOT(enterWaitState()));
QObject::connect(idleState, SIGNAL(entered()), this, SLOT(enterIdleState()));
void Client::setUser(const QString &user)
{
authCommand.setUser(user);
+ encryptCommand.setUser(user);
}
QString Client::pass() const
authCommand.setPass(pass);
}
+QString Client::apiKey() const
+{
+ return m_apiKey;
+}
+
+void Client::setApiKey(const QString &apiKey)
+{
+ m_apiKey = apiKey;
+}
+
+bool Client::encryptionEnabled() const
+{
+ return m_enableEncryption;
+}
+
+void Client::setEncryptionEnabled(bool enabled)
+{
+ m_enableEncryption = enabled;
+}
+
bool Client::compression() const
{
return authCommand.compression();
emit connectionError();
}
+void Client::enterEncryptionState()
+{
+#ifdef ANIDBUDPCLIENT_CLIENT_STATE_MACHINE_DEBUG
+qDebug() << "Entering Encryption State";
+#endif
+ if (encryptionStarted)
+ return;
+
+ encryptionStarted = true;
+
+ if (m_enableEncryption && !usingEncryption)
+ {
+ if (encryptReply != 0) encryptReply->deleteLater();
+ encryptReply = createReply(encryptCommand);
+ QObject::connect(encryptReply, SIGNAL(replyReady(bool)), this, SLOT(doEncrypt(bool)));
+ enqueueControlCommand(encryptReply, true);
+ return;
+ }
+ encryptionStarted = false;
+ emit encryptionEstablished();
+}
+
+void Client::doEncrypt(bool success)
+{
+#ifdef ANIDBUDPCLIENT_CLIENT_STATE_MACHINE_DEBUG
+qDebug() << "doEncrypt init";
+#endif
+ authenticatingStarted = false;
+ if (success)
+ {
+#ifdef ANIDBUDPCLIENT_CLIENT_STATE_MACHINE_DEBUG
+qDebug() << "success!";
+#endif
+ m_salt = encryptReply->salt();
+ usingEncryption = true;
+ emit encryptionEstablished();
+ return;
+ }
+
+ m_error = EncryptionError;
+ m_errorString = encryptReply->errorString();
+
+ emit connectionError();
+}
+
void Client::enterSendState()
{
#ifdef ANIDBUDPCLIENT_CLIENT_STATE_MACHINE_DEBUG
{
case DoNothingIdlePolicy:
m_sessionId = "";
+ m_salt = "";
+ usingEncryption = false;
break;
case KeepAliveIdlePolicy:
enqueueControlCommand(uptimeReply);
continue;
}
+ if (usingEncryption)
+ {
+#ifdef ANIDBUDPCLIENT_CLIENT_MISC_DEBUG
+qDebug() << "ENCRYPED DATAGRAM = " << data;
+#endif
+ tmp = decrypt(tmp);
+ }
+
if (authCommand.compression() && tmp.mid(0, 2) == "00")
{
#ifdef ANIDBUDPCLIENT_CLIENT_MISC_DEBUG
QRegExp rx("(?:50[34]|555|6[0-9]{2}) ");
if (rx.exactMatch(tmp.mid(0, 4)))
{
-
int replyCode = tmp.mid(0, 3).toInt();
switch (replyCode)
{
break;
case LOGGED_OUT:
m_sessionId.clear();
+ m_salt.clear();
+ usingEncryption = false;
break;
case CLIENT_VERSION_OUTDATED:
m_error = ClientVersionOutdatedError;
enqueueControlCommand(createReply(LogoutCommand()), force);
}
+#ifndef ANIDBUDPCLIENT_NO_ENCRYPT
+QByteArray Client::encrypt(const QByteArray &data)
+{
+ QCA::init();
+ if (!QCA::isSupported("aes128-ecb"))
+ {
+ qDebug() << "encryption failed due to no aes128-cbc";
+ return QByteArray();
+ }
+
+ QCA::SymmetricKey key(QCryptographicHash::hash((m_apiKey + m_salt).toUtf8(), QCryptographicHash::Md5));
+
+ // PKCS5 (or 7) padding
+ char pad = 16 - char(data.size() % 16);
+ QByteArray paddedData = data;
+ paddedData.reserve(data.size() + pad);
+ for (int i = 0; i < pad; ++i)
+ paddedData += pad;
+
+#ifdef ANIDBUDPCLIENT_CLIENT_ENCRYPT_DEBUG
+qDebug() << "PADDED DATA\n\t" << paddedData << "\n\t" << paddedData.toHex();
+#endif
+
+ QByteArray ret;
+ QCA::Cipher c("aes128", QCA::Cipher::ECB, QCA::Cipher::NoPadding, QCA::Encode, key);
+ ret = c.update(paddedData).toByteArray();
+ c.final(); // Should be empty as data is already padded
+// QCA::deinit();
+ return ret;
+}
+
+QByteArray Client::decrypt(const QByteArray &data)
+{
+ QCA::init();
+ if (!QCA::isSupported("aes128-ecb"))
+ {
+ qDebug() << "encryption failed due to no aes128-cbc";
+ return QByteArray();
+ }
+
+ QCA::SymmetricKey key(QCryptographicHash::hash((m_apiKey + m_salt).toUtf8(), QCryptographicHash::Md5));
+
+ QByteArray ret;
+#ifdef ANIDBUDPCLIENT_CLIENT_ENCRYPT_DEBUG
+qDebug() << " -- DECRYPT --";
+#endif
+ QCA::Cipher c("aes128", QCA::Cipher::ECB, QCA::Cipher::NoPadding, QCA::Decode, key);
+ ret = c.update(data).toByteArray();
+#ifdef ANIDBUDPCLIENT_CLIENT_ENCRYPT_DEBUG
+qDebug() << "PART 1 = " << ret;
+#endif
+ QByteArray p2;
+ p2 = c.final().toByteArray();
+#ifdef ANIDBUDPCLIENT_CLIENT_ENCRYPT_DEBUG
+qDebug() << "PART 2 = " << p2;
+#endif
+ ret += p2;
+
+ // Remove padding (and only it)
+ char pad = ret[ret.size() - 1];
+ if (pad > 0 && pad < 16)
+ {
+ QByteArray padding = ret.right(pad);
+ bool ok = true;
+ for (int i = 0; i < padding.size(); ++i)
+ {
+ if (padding[i] != pad)
+ {
+ ok = false;
+ break;
+ }
+ }
+ if (ok)
+ {
+ ret = ret.left(ret.size() - pad);
+ }
+ }
+// QCA::deinit();
+ return ret;
+}
+#endif
+
void Client::commandTimeout()
{
Q_ASSERT(!sentCommandOrder.isEmpty());
void Client::sendCommand(AbstractReply *command, bool controlCommand)
{
+ if (m_enableEncryption && command->command().requiresSession() && (m_salt.isEmpty() || !usingEncryption))
+ {
+ if (controlCommand)
+ enqueueControlCommand(command, true);
+ else
+ enqueueCommand(command, true);
+ emit startEncryption();
+ return;
+ }
+
if (m_sessionId.isEmpty() && command->command().requiresSession())
{
if (controlCommand)
if (m_sessionId.length())
datagram += "&s=" + m_sessionId;
-
command->setControlCommand(controlCommand);
command->setTimeSent();
}
#ifdef ANIDBUDPCLIENT_CLIENT_MISC_DEBUG
-qDebug() << QString("SENDING datagram:\n\t%1\nto: %2 ([%3]:%4)")
+qDebug() << QString("SENDING datagram:\n\t%1\nto: %2 ([%3]:%4) (%5 encryption)")
.arg(datagram.constData())
.arg(m_host)
.arg(m_hostAddress.toString())
- .arg(m_hostPort);
+ .arg(m_hostPort)
+ .arg(usingEncryption ? "with" : "without");
#endif
+ if (usingEncryption)
+ {
+ datagram = encrypt(datagram);
+
+#ifdef ANIDBUDPCLIENT_CLIENT_MISC_DEBUG
+qDebug() << QString("ENCRYPTED datagram:\n\t%1")
+ .arg(datagram.constData());
+#endif
+ }
+
socket->writeDatagram(datagram, m_hostAddress, m_hostPort);
}
--- /dev/null
+#include "encryptcommand.h"
+
+namespace AniDBUdpClient {
+
+EncryptCommand::EncryptCommand(const QString &user, EncryptionType encryptionType) : AbstractCommand(), m_user(user), m_encryptionType(encryptionType)
+{
+}
+
+QString EncryptCommand::user() const
+{
+ return m_user;
+}
+
+void EncryptCommand::setUser(const QString &user)
+{
+ m_user = user;
+}
+
+EncryptCommand::EncryptionType EncryptCommand::encryptionType() const
+{
+ return m_encryptionType;
+}
+
+void EncryptCommand::setEncryptionType(EncryptCommand::EncryptionType encryptionType)
+{
+ m_encryptionType = encryptionType;
+}
+
+Command EncryptCommand::rawCommand() const
+{
+ Command cmd;
+ cmd.first = "ENCRYPT";
+ cmd.second["user"] = m_user;
+ cmd.second["type"] = int(m_encryptionType);
+ return cmd;
+}
+
+bool EncryptCommand::waitForResult() const
+{
+ return true;
+}
+
+bool EncryptCommand::requiresSession() const
+{
+ return false;
+}
+
+// ===
+
+QString EncryptReply::salt()
+{
+ return m_salt;
+}
+
+QString EncryptReply::errorString()
+{
+ return m_errorString;
+}
+
+void EncryptReply::setRawReply(ReplyCode replyCode, const QString &reply)
+{
+ AbstractReply::setRawReply(replyCode, reply);
+
+ switch (replyCode)
+ {
+ case ENCRYPTION_ENABLED:
+ {
+ int saltEnd = reply.indexOf(' ');
+ if (saltEnd == -1)
+ {
+ signalReplyReady(false);
+ return;
+ }
+ m_salt = reply.left(reply.indexOf(' '));
+ if (m_salt.isEmpty())
+ {
+ signalReplyReady(false);
+ return;
+ }
+ signalReplyReady(true);
+ }
+ break;
+ case API_PASSWORD_NOT_DEFINED:
+ m_errorString = tr("API password not defined.");
+ signalReplyReady(false);
+ break;
+ case NO_SUCH_ENCRYPTION_TYPE:
+ m_errorString = tr("No such encryption type.");
+ signalReplyReady(false);
+ break;
+ case NO_SUCH_USER:
+ m_errorString = tr("No such user");
+ signalReplyReady(false);
+ break;
+ default:
+ signalReplyReady(false);
+ break;
+ }
+}
+
+} // namespace AniDBUdpClient