diff options
-rw-r--r-- | CMakeLists.txt | 3 | ||||
-rw-r--r-- | lib/connection.cpp | 104 | ||||
-rw-r--r-- | lib/connection.h | 20 | ||||
-rw-r--r-- | lib/database.cpp | 30 | ||||
-rw-r--r-- | lib/database.h | 4 | ||||
-rw-r--r-- | lib/events/encryptedevent.h | 2 | ||||
-rw-r--r-- | lib/events/keyverificationevent.cpp | 32 | ||||
-rw-r--r-- | lib/events/keyverificationevent.h | 36 | ||||
-rw-r--r-- | lib/events/roomevent.h | 2 | ||||
-rw-r--r-- | lib/keyverificationsession.cpp | 522 | ||||
-rw-r--r-- | lib/keyverificationsession.h | 140 | ||||
-rw-r--r-- | res.qrc | 5 | ||||
-rw-r--r-- | sas-emoji.json | 2178 |
13 files changed, 3066 insertions, 12 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 15726240..1030e12d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,6 +73,7 @@ message(STATUS " Header files will be installed to ${CMAKE_INSTALL_PREFIX}/${${ # Instruct CMake to run moc automatically when needed. set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) option(BUILD_WITH_QT6 "Build Quotient with Qt 6 (EXPERIMENTAL)" OFF) @@ -138,6 +139,7 @@ list(APPEND lib_SRCS lib/eventitem.h lib/eventitem.cpp lib/accountregistry.h lib/accountregistry.cpp lib/mxcreply.h lib/mxcreply.cpp + lib/keyverificationsession.h lib/keyverificationsession.cpp lib/events/event.h lib/events/event.cpp lib/events/eventloader.h lib/events/roomevent.h lib/events/roomevent.cpp @@ -171,6 +173,7 @@ list(APPEND lib_SRCS lib/jobs/syncjob.h lib/jobs/syncjob.cpp lib/jobs/mediathumbnailjob.h lib/jobs/mediathumbnailjob.cpp lib/jobs/downloadfilejob.h lib/jobs/downloadfilejob.cpp + res.qrc ) if (${PROJECT_NAME}_ENABLE_E2EE) list(APPEND lib_SRCS diff --git a/lib/connection.cpp b/lib/connection.cpp index 42a5f5fc..68aed4e4 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -40,6 +40,8 @@ # include "e2ee/qolmutils.h" # include "database.h" # include "e2ee/qolminboundsession.h" +# include "events/keyverificationevent.h" +# include "keyverificationsession.h" #if QT_VERSION_MAJOR >= 6 # include <qt6keychain/keychain.h> @@ -974,8 +976,23 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) outdatedUsers += event.senderId(); encryptionUpdateRequired = true; pendingEncryptedEvents.push_back(std::make_unique<EncryptedEvent>(event.fullJson())); - }, [](const Event& e){ - // Unhandled + }, [this](const KeyVerificationRequestEvent& event) { + auto session = new KeyVerificationSession(q->userId(), event, q, false, q); + emit q->newKeyVerificationSession(session); + }, [this](const KeyVerificationReadyEvent& event) { + emit q->incomingKeyVerificationReady(event); + }, [this](const KeyVerificationStartEvent& event) { + emit q->incomingKeyVerificationStart(event); + }, [this](const KeyVerificationAcceptEvent& event) { + emit q->incomingKeyVerificationAccept(event); + }, [this](const KeyVerificationKeyEvent& event) { + emit q->incomingKeyVerificationKey(event); + }, [this](const KeyVerificationMacEvent& event) { + emit q->incomingKeyVerificationMac(event); + }, [this](const KeyVerificationDoneEvent& event) { + emit q->incomingKeyVerificationDone(event); + }, [this](const KeyVerificationCancelEvent& event) { + emit q->incomingKeyVerificationCancel(event); }); } #endif @@ -998,9 +1015,25 @@ void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& eve qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId() << "is not found at the connection" << q->objectName(); } - }, - [](const Event& evt) { - qCDebug(E2EE) << "Skipping encrypted to_device event, type" + }, [this](const KeyVerificationRequestEvent& event) { + auto session = new KeyVerificationSession(q->userId(), event, q, true, q); + emit q->newKeyVerificationSession(session); + }, [this](const KeyVerificationReadyEvent& event) { + emit q->incomingKeyVerificationReady(event); + }, [this](const KeyVerificationStartEvent& event) { + emit q->incomingKeyVerificationStart(event); + }, [this](const KeyVerificationAcceptEvent& event) { + emit q->incomingKeyVerificationAccept(event); + }, [this](const KeyVerificationKeyEvent& event) { + emit q->incomingKeyVerificationKey(event); + }, [this](const KeyVerificationMacEvent& event) { + emit q->incomingKeyVerificationMac(event); + }, [this](const KeyVerificationDoneEvent& event) { + emit q->incomingKeyVerificationDone(event); + }, [this](const KeyVerificationCancelEvent& event) { + emit q->incomingKeyVerificationCancel(event); + }, [](const Event& evt) { + qCWarning(E2EE) << "Skipping encrypted to_device event, type" << evt.matrixType(); }); } @@ -2127,8 +2160,8 @@ void Connection::Private::saveDevicesList() query.prepare(QStringLiteral( "INSERT INTO tracked_devices" - "(matrixId, deviceId, curveKeyId, curveKey, edKeyId, edKey) " - "VALUES(:matrixId, :deviceId, :curveKeyId, :curveKey, :edKeyId, :edKey);" + "(matrixId, deviceId, curveKeyId, curveKey, edKeyId, edKey, verified) " + "SELECT :matrixId, :deviceId, :curveKeyId, :curveKey, :edKeyId, :edKey, :verified WHERE NOT EXISTS(SELECT 1 FROM tracked_devices WHERE matrixId=:matrixId AND deviceId=:deviceId);" )); for (const auto& user : deviceKeys.keys()) { for (const auto& device : deviceKeys[user]) { @@ -2142,6 +2175,8 @@ void Connection::Private::saveDevicesList() query.bindValue(":curveKey", device.keys[curveKeyId]); query.bindValue(":edKeyId", edKeyId); query.bindValue(":edKey", device.keys[edKeyId]); + // If the device gets saved here, it can't be verified + query.bindValue(":verified", false); q->database()->execute(query); } @@ -2297,3 +2332,58 @@ bool Connection::isKnownCurveKey(const QString& user, const QString& curveKey) } #endif + +void Connection::startKeyVerificationSession(const QString& deviceId) +{ + auto session = new KeyVerificationSession(userId(), deviceId, this, this); + Q_EMIT newKeyVerificationSession(session); +} + +void Connection::sendToDevice(const QString& userId, const QString& deviceId, event_ptr_tt<Event> event, bool encrypted) +{ + + UsersToDevicesToEvents payload; + if (encrypted) { + QJsonObject payloadJson = event->fullJson(); + payloadJson["recipient"] = userId; + payloadJson["sender"] = user()->id(); + QJsonObject recipientObject; + recipientObject["ed25519"] = edKeyForUserDevice(userId, deviceId); + payloadJson["recipient_keys"] = recipientObject; + QJsonObject senderObject; + senderObject["ed25519"] = QString(olmAccount()->identityKeys().ed25519); + payloadJson["keys"] = senderObject; + + const auto& u = user(userId); + auto cipherText = olmEncryptMessage(u, deviceId, QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); + QJsonObject encryptedJson; + encryptedJson[curveKeyForUserDevice(userId, deviceId)] = QJsonObject{{"type", cipherText.first}, {"body", QString(cipherText.second)}, {"sender", this->userId()}}; + auto encryptedEvent = makeEvent<EncryptedEvent>(encryptedJson, olmAccount()->identityKeys().curve25519); + payload[userId][deviceId] = std::move(encryptedEvent); + } else { + payload[userId][deviceId] = std::move(event); + } + sendToDevices(payload[userId][deviceId]->matrixType(), payload); +} + +bool Connection::isVerifiedSession(const QString& megolmSessionId) +{ + auto query = database()->prepareQuery("SELECT olmSessionId FROM inbound_megolm_sessions WHERE sessionId=:sessionId;"_ls); + query.bindValue(":sessionId", megolmSessionId); + database()->execute(query); + if (!query.next()) { + return false; + } + auto olmSessionId = query.value("olmSessionId").toString(); + query.prepare("SELECT senderKey FROM olm_sessions WHERE sessionId=:sessionId;"_ls); + query.bindValue(":sessionId", olmSessionId); + database()->execute(query); + if (!query.next()) { + return false; + } + auto curveKey = query.value("senderKey"_ls).toString(); + query.prepare("SELECT verified FROM tracked_devices WHERE curveKey=:curveKey;"_ls); + query.bindValue(":curveKey", curveKey); + database()->execute(query); + return query.next() && query.value("verified").toBool(); +} diff --git a/lib/connection.h b/lib/connection.h index 12db2e30..fc189ac4 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -26,6 +26,8 @@ #include "e2ee/e2ee.h" #include "e2ee/qolmmessage.h" #include "e2ee/qolmoutboundsession.h" +#include "events/keyverificationevent.h" +#include "keyverificationsession.h" #endif Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow) @@ -324,6 +326,8 @@ public: QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(Room* room); void saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data); + /// Returns true if this megolm session comes from a verified device + bool isVerifiedSession(const QString& megolmSessionId); //This assumes that an olm session with (user, device) exists QPair<QOlmMessage::Type, QByteArray> olmEncryptMessage(User* user, const QString& device, const QByteArray& message); @@ -512,6 +516,9 @@ public: /// Saves the olm account data to disk. Usually doesn't need to be called manually. void saveOlmAccount(); + // This assumes that an olm session already exists. If it doesn't, no message is sent. + void sendToDevice(const QString& userId, const QString& deviceId, event_ptr_tt<Event> event, bool encrypted); + public Q_SLOTS: /// \brief Set the homeserver base URL and retrieve its login flows /// @@ -688,6 +695,8 @@ public Q_SLOTS: /** \deprecated Do not use this directly, use Room::leaveRoom() instead */ virtual LeaveRoomJob* leaveRoom(Room* room); + void startKeyVerificationSession(const QString& deviceId); + #ifdef Quotient_E2EE_ENABLED void encryptionUpdate(Room *room); PicklingMode picklingMode() const; @@ -698,6 +707,7 @@ public Q_SLOTS: QString edKeyForUserDevice(const QString& user, const QString& device) const; bool isKnownCurveKey(const QString& user, const QString& curveKey); #endif + Q_SIGNALS: /// \brief Initial server resolution has failed /// @@ -855,6 +865,16 @@ Q_SIGNALS: void lazyLoadingChanged(); void turnServersChanged(const QJsonObject& servers); void devicesListLoaded(); + void incomingKeyVerificationReady(const KeyVerificationReadyEvent& event); + void incomingKeyVerificationStart(const KeyVerificationStartEvent& event); + void incomingKeyVerificationAccept(const KeyVerificationAcceptEvent& event); + void incomingKeyVerificationKey(const KeyVerificationKeyEvent& event); + void incomingKeyVerificationMac(const KeyVerificationMacEvent& event); + void incomingKeyVerificationDone(const KeyVerificationDoneEvent& event); + void incomingKeyVerificationCancel(const KeyVerificationCancelEvent& event); + + void newKeyVerificationSession(KeyVerificationSession* session); + void sessionVerified(const QString& userId, const QString& deviceId); protected: /** diff --git a/lib/database.cpp b/lib/database.cpp index 902d0487..a85d96bb 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -35,6 +35,7 @@ Database::Database(const QString& matrixId, const QString& deviceId, QObject* pa case 1: migrateTo2(); case 2: migrateTo3(); case 3: migrateTo4(); + case 4: migrateTo5(); } } @@ -105,6 +106,7 @@ void Database::migrateTo2() { qCDebug(DATABASE) << "Migrating database to version 2"; transaction(); + execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD ed25519Key TEXT")); execute(QStringLiteral("ALTER TABLE olm_sessions ADD lastReceived TEXT")); @@ -133,7 +135,7 @@ void Database::migrateTo3() void Database::migrateTo4() { - qCDebug(DATABASE) << "Migrating database to ersion 4"; + qCDebug(DATABASE) << "Migrating database to version 4"; transaction(); execute(QStringLiteral("CREATE TABLE sent_megolm_sessions (roomId TEXT, userId TEXT, deviceId TEXT, identityKey TEXT, sessionId TEXT, i INTEGER);")); @@ -143,6 +145,16 @@ void Database::migrateTo4() commit(); } +void Database::migrateTo5() +{ + qCDebug(DATABASE) << "Migrating database to version 5"; + transaction(); + + execute(QStringLiteral("ALTER TABLE tracked_devices ADD verified BOOL;")); + execute(QStringLiteral("PRAGMA user_version = 5")); + commit(); +} + QByteArray Database::accountPickle() { auto query = prepareQuery(QStringLiteral("SELECT pickle FROM accounts;")); @@ -396,3 +408,19 @@ void Database::updateOlmSession(const QString& senderKey, const QString& session commit(); } +void Database::setSessionVerified(const QString& edKeyId) +{ + auto query = prepareQuery(QStringLiteral("UPDATE tracked_devices SET verified=true WHERE edKeyId=:edKeyId;")); + query.bindValue(":edKeyId", edKeyId); + transaction(); + execute(query); + commit(); +} + +bool Database::isSessionVerified(const QString& edKey) +{ + auto query = prepareQuery(QStringLiteral("SELECT verified FROM tracked_devices WHERE edKey=:edKey")); + query.bindValue(":edKey", edKey); + execute(query); + return query.next() && query.value("verified").toBool(); +} diff --git a/lib/database.h b/lib/database.h index 3eb26b0a..afc41e42 100644 --- a/lib/database.h +++ b/lib/database.h @@ -50,11 +50,15 @@ public: QHash<QString, QStringList> devicesWithoutKey(Room* room, const QString &sessionId); void setDevicesReceivedKey(const QString& roomId, QHash<User *, QStringList> devices, const QString& sessionId, int index); + bool isSessionVerified(const QString& edKey); + void setSessionVerified(const QString& edKeyId); + private: void migrateTo1(); void migrateTo2(); void migrateTo3(); void migrateTo4(); + void migrateTo5(); QString m_matrixId; }; diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index ddd5e415..bfacdec9 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -58,6 +58,8 @@ public: RoomEventPtr createDecrypted(const QString &decrypted) const; void setRelation(const QJsonObject& relation); + + bool isVerified(); }; REGISTER_EVENT_TYPE(EncryptedEvent) diff --git a/lib/events/keyverificationevent.cpp b/lib/events/keyverificationevent.cpp index 4803955d..e7f5b019 100644 --- a/lib/events/keyverificationevent.cpp +++ b/lib/events/keyverificationevent.cpp @@ -106,7 +106,7 @@ QStringList KeyVerificationAcceptEvent::shortAuthenticationString() const return contentPart<QStringList>("short_authentification_string"_ls); } -QString KeyVerificationAcceptEvent::commitement() const +QString KeyVerificationAcceptEvent::commitment() const { return contentPart<QString>("commitment"_ls); } @@ -162,3 +162,33 @@ QHash<QString, QString> KeyVerificationMacEvent::mac() const { return contentPart<QHash<QString, QString>>("mac"_ls); } + +KeyVerificationDoneEvent::KeyVerificationDoneEvent(const QJsonObject &obj) + : Event(typeId(), obj) +{ +} + +QString KeyVerificationDoneEvent::transactionId() const +{ + return contentPart<QString>("transaction_id"_ls); +} + + +KeyVerificationReadyEvent::KeyVerificationReadyEvent(const QJsonObject &obj) + : Event(typeId(), obj) +{} + +QString KeyVerificationReadyEvent::fromDevice() const +{ + return contentPart<QString>("from_device"_ls); +} + +QString KeyVerificationReadyEvent::transactionId() const +{ + return contentPart<QString>("transaction_id"_ls); +} + +QStringList KeyVerificationReadyEvent::methods() const +{ + return contentPart<QStringList>("methods"_ls); +} diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h index 497e56a2..a9f63968 100644 --- a/lib/events/keyverificationevent.h +++ b/lib/events/keyverificationevent.h @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> // SPDX-License-Identifier: LGPL-2.1-or-later +#pragma once + #include "event.h" namespace Quotient { @@ -31,6 +33,24 @@ public: }; REGISTER_EVENT_TYPE(KeyVerificationRequestEvent) +class QUOTIENT_API KeyVerificationReadyEvent : public Event { +public: + DEFINE_EVENT_TYPEID("m.key.verification.ready", KeyVerificationReadyEvent) + + explicit KeyVerificationReadyEvent(const QJsonObject& obj); + + /// The device ID which is accepting the request. + QString fromDevice() const; + + /// The transaction id of the verification request + QString transactionId() const; + + /// The verification methods supported by the sender. + QStringList methods() const; +}; +REGISTER_EVENT_TYPE(KeyVerificationReadyEvent) + + /// Begins a key verification process. class QUOTIENT_API KeyVerificationStartEvent : public Event { public: @@ -104,7 +124,7 @@ public: /// The hash (encoded as unpadded base64) of the concatenation of the /// device's ephemeral public key (encoded as unpadded base64) and the /// canonical JSON representation of the m.key.verification.start message. - QString commitement() const; + QString commitment() const; }; REGISTER_EVENT_TYPE(KeyVerificationAcceptEvent) @@ -128,7 +148,7 @@ REGISTER_EVENT_TYPE(KeyVerificationCancelEvent) /// Sends the ephemeral public key for a device to the partner device. /// Typically sent as a to-device event. -class KeyVerificationKeyEvent : public Event { +class QUOTIENT_API KeyVerificationKeyEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.key", KeyVerificationKeyEvent) @@ -158,4 +178,16 @@ public: QHash<QString, QString> mac() const; }; REGISTER_EVENT_TYPE(KeyVerificationMacEvent) + +class QUOTIENT_API KeyVerificationDoneEvent : public Event { +public: + DEFINE_EVENT_TYPEID("m.key.verification.done", KeyVerificationRequestEvent) + + explicit KeyVerificationDoneEvent(const QJsonObject& obj); + + /// The same transactionId as before + QString transactionId() const; +}; +REGISTER_EVENT_TYPE(KeyVerificationDoneEvent) + } // namespace Quotient diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index a7d6c428..5670f55f 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -62,7 +62,7 @@ public: #ifdef Quotient_E2EE_ENABLED void setOriginalEvent(event_ptr_tt<RoomEvent>&& originalEvent); - const RoomEvent* originalEvent() { return _originalEvent.get(); } + const RoomEvent* originalEvent() const { return _originalEvent.get(); } const QJsonObject encryptedJson() const; #endif diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp new file mode 100644 index 00000000..3b3b7627 --- /dev/null +++ b/lib/keyverificationsession.cpp @@ -0,0 +1,522 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "connection.h" +#include "keyverificationsession.h" +#include "olm/sas.h" +#include "e2ee/qolmaccount.h" +#include "e2ee/qolmutils.h" +#include "events/event.h" +#include <QtCore/QCryptographicHash> +#include <QtCore/QUuid> +#include <QtCore/QTimer> +#include "database.h" + +using namespace Quotient; + +KeyVerificationSession::KeyVerificationSession(const QString& remoteUserId, const KeyVerificationRequestEvent& event, Connection *connection, bool encrypted, QObject* parent) + : QObject(parent) + , m_remoteUserId(remoteUserId) + , m_remoteDeviceId(event.fromDevice()) + , m_transactionId(event.transactionId()) + , m_connection(connection) + , m_encrypted(encrypted) + , m_remoteSupportedMethods(event.methods()) +{ + auto timeoutTime = std::min<long int>(event.timestamp() + 600000, QDateTime::currentDateTime().addSecs(120).toMSecsSinceEpoch()); + m_timeout = timeoutTime - QDateTime::currentMSecsSinceEpoch(); + if (m_timeout <= 5000) { + return; + } + init(); + setState(INCOMING); +} + +KeyVerificationSession::KeyVerificationSession(const QString& userId, const QString& deviceId, Connection* connection, QObject* parent) + : QObject(parent) + , m_remoteUserId(userId) + , m_remoteDeviceId(deviceId) + , m_transactionId(QUuid::createUuid().toString()) + , m_connection(connection) + , m_encrypted(false) +{ + m_timeout = 600000; + init(); + QMetaObject::invokeMethod(this, &KeyVerificationSession::sendRequest); +} + +void KeyVerificationSession::init() +{ + connect(m_connection, &Connection::incomingKeyVerificationReady, this, [this](const KeyVerificationReadyEvent& event) { + if (event.transactionId() == m_transactionId && event.fromDevice() == m_remoteDeviceId) { + handleReady(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationStart, this, [this](const KeyVerificationStartEvent& event) { + if (event.transactionId() == m_transactionId && event.fromDevice() == m_remoteDeviceId) { + handleStart(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationAccept, this, [this](const KeyVerificationAcceptEvent& event) { + if (event.transactionId() == m_transactionId) { + handleAccept(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationKey, this, [this](const KeyVerificationKeyEvent& event) { + if (event.transactionId() == m_transactionId) { + handleKey(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationMac, this, [this](const KeyVerificationMacEvent& event) { + if (event.transactionId() == m_transactionId) { + handleMac(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationDone, this, [this](const KeyVerificationDoneEvent& event) { + if (event.transactionId() == m_transactionId) { + handleDone(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationCancel, this, [this](const KeyVerificationCancelEvent& event) { + if (event.transactionId() == m_transactionId) { + handleCancel(event); + } + }); + + QTimer::singleShot(m_timeout, this, [this] { + cancelVerification(TIMEOUT); + }); + + + m_sas = olm_sas(new uint8_t[olm_sas_size()]); + auto randomSize = olm_create_sas_random_length(m_sas); + auto random = getRandom(randomSize); + olm_create_sas(m_sas, random.data(), randomSize); + + m_language = QLocale::system().uiLanguages()[0]; + m_language = m_language.left(m_language.indexOf('-')); +} + +KeyVerificationSession::~KeyVerificationSession() +{ + delete[] reinterpret_cast<uint8_t*>(m_sas); +} + +void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) +{ + if (state() != WAITINGFORKEY && state() != WAITINGFORVERIFICATION) { + cancelVerification(UNEXPECTED_MESSAGE); + return; + } + olm_sas_set_their_key(m_sas, event.key().toLatin1().data(), event.key().toLatin1().size()); + + if (startSentByUs) { + auto commitment = QString(QCryptographicHash::hash((event.key() % m_startEvent).toLatin1(), QCryptographicHash::Sha256).toBase64()); + commitment = commitment.left(commitment.indexOf('=')); + if (commitment != m_commitment) { + qCWarning(E2EE) << "Commitment mismatch; aborting verification"; + cancelVerification(MISMATCHED_COMMITMENT); + return; + } + } else { + sendKey(); + } + setState(WAITINGFORVERIFICATION); + + QByteArray keyBytes(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, keyBytes.data(), keyBytes.size()); + QString key = QString(keyBytes); + + QByteArray output(6, '\0'); + QString infoTemplate = startSentByUs ? "MATRIX_KEY_VERIFICATION_SAS|%1|%2|%3|%4|%5|%6|%7"_ls : "MATRIX_KEY_VERIFICATION_SAS|%4|%5|%6|%1|%2|%3|%7"_ls; + + auto info = infoTemplate.arg(m_connection->userId()).arg(m_connection->deviceId()).arg(key).arg(m_remoteUserId).arg(m_remoteDeviceId).arg(event.key()).arg(m_transactionId); + olm_sas_generate_bytes(m_sas, info.toLatin1().data(), info.toLatin1().size(), output.data(), output.size()); + + QVector<uint8_t> code(7, 0); + const auto& data = (uint8_t *) output.data(); + + code[0] = data[0] >> 2; + code[1] = (data[0] << 4 & 0x3f) | data[1] >> 4; + code[2] = (data[1] << 2 & 0x3f) | data[2] >> 6; + code[3] = data[2] & 0x3f; + code[4] = data[3] >> 2; + code[5] = (data[3] << 4 & 0x3f) | data[4] >> 4; + code[6] = (data[4] << 2 & 0x3f) | data[5] >> 6; + + for (const auto& c : code) { + auto [emoji, description] = emojiForCode(c); + QVariantMap map; + map["emoji"] = emoji; + map["description"] = description; + m_sasEmojis += map; + } + emit sasEmojisChanged(); + emit keyReceived(); +} + +QByteArray KeyVerificationSession::macInfo(bool verifying, const QString& key) +{ + return (verifying ? "MATRIX_KEY_VERIFICATION_MAC%3%4%1%2%5%6"_ls : "MATRIX_KEY_VERIFICATION_MAC%1%2%3%4%5%6"_ls).arg(m_connection->userId()).arg(m_connection->deviceId()).arg(m_remoteUserId).arg(m_remoteDeviceId).arg(m_transactionId).arg(key).toLatin1(); +} + +QString KeyVerificationSession::calculateMac(const QString& input, bool verifying, const QString& keyId) +{ + QByteArray inputBytes = input.toLatin1(); + QByteArray outputBytes(olm_sas_mac_length(m_sas), '\0'); + olm_sas_calculate_mac(m_sas, inputBytes.data(), inputBytes.size(), macInfo(verifying, keyId).data(), macInfo(verifying, keyId).size(), outputBytes.data(), outputBytes.size()); + auto output = QString(outputBytes); + return output.left(output.indexOf('=')); +} + +void KeyVerificationSession::sendMac() +{ + QString edKeyId = "ed25519:" % m_connection->deviceId(); + + auto keys = calculateMac(edKeyId, false); + + QJsonObject mac; + auto key = m_connection->olmAccount()->deviceKeys().keys[edKeyId]; + mac[edKeyId] = calculateMac(key, false, edKeyId); + + auto event = makeEvent<KeyVerificationMacEvent>(QJsonObject { + {"type", "m.key.verification.mac"}, + {"content", QJsonObject{ + {"transaction_id", m_transactionId}, + {"keys", keys}, + {"mac", mac}, + }} + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + setState (macReceived ? DONE : WAITINGFORMAC); +} + +void KeyVerificationSession::sendDone() +{ + auto event = makeEvent<KeyVerificationDoneEvent>(QJsonObject { + {"type", "m.key.verification.done"}, + {"content", QJsonObject{ + {"transaction_id", m_transactionId}, + }} + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); +} + +void KeyVerificationSession::sendKey() +{ + QByteArray keyBytes(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, keyBytes.data(), keyBytes.size()); + QString key = QString(keyBytes); + auto event = makeEvent<KeyVerificationKeyEvent>(QJsonObject { + {"type", "m.key.verification.key"}, + {"content", QJsonObject{ + {"transaction_id", m_transactionId}, + {"key", key}, + }} + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); +} + + +void KeyVerificationSession::cancelVerification(Error error) +{ + auto event = makeEvent<KeyVerificationCancelEvent>(QJsonObject { + {"type", "m.key.verification.cancel"}, + {"content", QJsonObject{ + {"code", errorToString(error)}, + {"reason", errorToString(error)}, + {"transaction_id", m_transactionId} + }} + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + setState(CANCELED); + setError(error); + emit finished(); + deleteLater(); +} + +void KeyVerificationSession::sendReady() +{ + auto methods = commonSupportedMethods(m_remoteSupportedMethods); + + if (methods.isEmpty()) { + cancelVerification(UNKNOWN_METHOD); + return; + } + + auto event = makeEvent<KeyVerificationReadyEvent>(QJsonObject { + {"type", "m.key.verification.ready"}, + {"content", QJsonObject { + {"from_device", m_connection->deviceId()}, + {"methods", toJson(methods)}, + {"transaction_id", m_transactionId}, + }} + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + setState(READY); + + if (methods.size() == 1) { + sendStartSas(); + } +} + +void KeyVerificationSession::sendStartSas() +{ + startSentByUs = true; + auto event = makeEvent<KeyVerificationStartEvent>(QJsonObject { + {"type", "m.key.verification.start"}, + {"content", QJsonObject { + {"from_device", m_connection->deviceId()}, + {"hashes", QJsonArray {"sha256"}}, + {"key_agreement_protocols", QJsonArray { "curve25519-hkdf-sha256" }}, + {"message_authentication_codes", QJsonArray { "hkdf-hmac-sha256" }}, + {"method", "m.sas.v1"}, + {"short_authentication_string", QJsonArray { "decimal", "emoji" }}, + {"transaction_id", m_transactionId}, + }} + }); + m_startEvent = QJsonDocument(event->contentJson()).toJson(QJsonDocument::Compact); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + setState(WAITINGFORACCEPT); +} + +void KeyVerificationSession::handleReady(const KeyVerificationReadyEvent& event) +{ + if (state() != WAITINGFORREADY) { + cancelVerification(UNEXPECTED_MESSAGE); + return; + } + setState(READY); + m_remoteSupportedMethods = event.methods(); + auto methods = commonSupportedMethods(m_remoteSupportedMethods); + + if (methods.isEmpty()) { + cancelVerification(UNKNOWN_METHOD); + return; + } + + if (methods.size() == 1) { + sendStartSas(); + } +} + +void KeyVerificationSession::handleStart(const KeyVerificationStartEvent& event) +{ + if (state() != READY) { + cancelVerification(UNEXPECTED_MESSAGE); + return; + } + if (startSentByUs) { + if (m_remoteUserId > m_connection->userId() || (m_remoteUserId == m_connection->userId() && m_remoteDeviceId > m_connection->deviceId())) { + return; + } else { + startSentByUs = false; + } + } + QByteArray publicKey(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, publicKey.data(), publicKey.size()); + const auto canonicalEvent = QString(QJsonDocument(event.contentJson()).toJson(QJsonDocument::Compact)); + auto commitment = QString(QCryptographicHash::hash((QString(publicKey) % canonicalEvent).toLatin1(), QCryptographicHash::Sha256).toBase64()); + commitment = commitment.left(commitment.indexOf('=')); + + auto acceptEvent = makeEvent<KeyVerificationAcceptEvent>(QJsonObject { + {"type", "m.key.verification.accept"}, + {"content", QJsonObject { + {"commitment", commitment}, + {"hash", "sha256"}, + {"key_agreement_protocol", "curve25519-hkdf-sha256"}, + {"message_authentication_code", "hkdf-hmac-sha256"}, + {"method", "m.sas.v1"}, + {"short_authentication_string", QJsonArray { + "decimal", + "emoji", + }}, + {"transaction_id", m_transactionId}, + }} + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(acceptEvent), m_encrypted); + setState(ACCEPTED); +} + +void KeyVerificationSession::handleAccept(const KeyVerificationAcceptEvent& event) +{ + if(state() != WAITINGFORACCEPT) { + cancelVerification(UNEXPECTED_MESSAGE); + return; + } + m_commitment = event.commitment(); + sendKey(); + setState(WAITINGFORKEY); +} + +void KeyVerificationSession::handleMac(const KeyVerificationMacEvent& event) +{ + QStringList keys = event.mac().keys(); + keys.sort(); + const auto& key = keys.join(","); + const QString edKeyId = "ed25519:"_ls % m_remoteDeviceId; + + if (calculateMac(m_connection->edKeyForUserDevice(m_remoteUserId, m_remoteDeviceId), true, edKeyId) != event.mac()[edKeyId]) { + cancelVerification(KEY_MISMATCH); + return; + } + + if (calculateMac(key, true) != event.keys()) { + cancelVerification(KEY_MISMATCH); + return; + } + + m_connection->database()->setSessionVerified(edKeyId); + emit m_connection->sessionVerified(m_remoteUserId, m_remoteDeviceId); + macReceived = true; + + if (state() == WAITINGFORMAC) { + setState(DONE); + sendDone(); + emit finished(); + deleteLater(); + } +} + +void KeyVerificationSession::handleDone(const KeyVerificationDoneEvent& event) +{ + if (state() != DONE) { + cancelVerification(UNEXPECTED_MESSAGE); + } +} + +void KeyVerificationSession::handleCancel(const KeyVerificationCancelEvent& event) +{ + setError(stringToError(event.code())); + setState(CANCELED); +} + +std::pair<QString, QString> KeyVerificationSession::emojiForCode(int code) +{ + static QJsonArray data; + if (data.isEmpty()) { + QFile dataFile(":/sas-emoji.json"); + dataFile.open(QFile::ReadOnly); + data = QJsonDocument::fromJson(dataFile.readAll()).array(); + } + if (data[code].toObject()["translated_descriptions"].toObject().contains(m_language)) { + return {data[code].toObject()["emoji"].toString(), data[code].toObject()["translated_descriptions"].toObject()[m_language].toString()}; + } + return {data[code].toObject()["emoji"].toString(), data[code].toObject()["description"].toString()}; +} + +QList<QVariantMap> KeyVerificationSession::sasEmojis() const +{ + return m_sasEmojis; +} + +void KeyVerificationSession::sendRequest() +{ + QJsonArray methods = toJson(m_supportedMethods); + auto event = makeEvent<KeyVerificationRequestEvent>(QJsonObject { + {"type", "m.key.verification.request"}, + {"content", QJsonObject { + {"from_device", m_connection->deviceId()}, + {"transaction_id", m_transactionId}, + {"methods", methods}, + {"timestamp", QDateTime::currentMSecsSinceEpoch()}, + }}, + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + setState(WAITINGFORREADY); +} + +KeyVerificationSession::State KeyVerificationSession::state() const +{ + return m_state; +} + +void KeyVerificationSession::setState(KeyVerificationSession::State state) +{ + m_state = state; + emit stateChanged(); +} + +KeyVerificationSession::Error KeyVerificationSession::error() const +{ + return m_error; +} + +void KeyVerificationSession::setError(Error error) +{ + m_error = error; + emit errorChanged(); +} + +QString KeyVerificationSession::errorToString(Error error) const +{ + switch(error) { + case NONE: + return "none"_ls; + case TIMEOUT: + return "m.timeout"_ls; + case USER: + return "m.user"_ls; + case UNEXPECTED_MESSAGE: + return "m.unexpected_message"_ls; + case UNKNOWN_TRANSACTION: + return "m.unknown_transaction"_ls; + case UNKNOWN_METHOD: + return "m.unknown_method"_ls; + case KEY_MISMATCH: + return "m.key_mismatch"_ls; + case USER_MISMATCH: + return "m.user_mismatch"_ls; + case INVALID_MESSAGE: + return "m.invalid_message"_ls; + case SESSION_ACCEPTED: + return "m.accepted"_ls; + case MISMATCHED_COMMITMENT: + return "m.mismatched_commitment"_ls; + case MISMATCHED_SAS: + return "m.mismatched_sas"_ls; + default: + return "m.user"_ls; + } +} + +KeyVerificationSession::Error KeyVerificationSession::stringToError(const QString& error) const +{ + if (error == "m.timeout"_ls) { + return REMOTE_TIMEOUT; + } else if (error == "m.user"_ls) { + return REMOTE_USER; + } else if (error == "m.unexpected_message"_ls) { + return REMOTE_UNEXPECTED_MESSAGE; + } else if (error == "m.unknown_message"_ls) { + return REMOTE_UNEXPECTED_MESSAGE; + } else if (error == "m.unknown_transaction"_ls) { + return REMOTE_UNKNOWN_TRANSACTION; + } else if (error == "m.unknown_method"_ls) { + return REMOTE_UNKNOWN_METHOD; + } else if (error == "m.key_mismatch"_ls) { + return REMOTE_KEY_MISMATCH; + } else if (error == "m.user_mismatch"_ls) { + return REMOTE_USER_MISMATCH; + } else if (error == "m.invalid_message"_ls) { + return REMOTE_INVALID_MESSAGE; + } else if (error == "m.accepted"_ls) { + return REMOTE_SESSION_ACCEPTED; + } else if (error == "m.mismatched_commitment"_ls) { + return REMOTE_MISMATCHED_COMMITMENT; + } else if (error == "m.mismatched_sas"_ls) { + return REMOTE_MISMATCHED_SAS; + } + return NONE; +} + +QStringList KeyVerificationSession::commonSupportedMethods(const QStringList& remoteMethods) const +{ + QStringList result; + for (const auto& method : remoteMethods) { + if (m_supportedMethods.contains(method)) { + result += method; + } + } + return result; +} diff --git a/lib/keyverificationsession.h b/lib/keyverificationsession.h new file mode 100644 index 00000000..cb7a99e9 --- /dev/null +++ b/lib/keyverificationsession.h @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "events/keyverificationevent.h" +#include <QtCore/QObject> +#include <qchar.h> + +class OlmSAS; + +namespace Quotient { +class Connection; + +/** A key verification session. Listen for incoming sessions by connecting to Connection::newKeyVerificationSession. + Start a new session using Connection::startKeyVerificationSession. + The object is delete after finished is emitted. +*/ +class QUOTIENT_API KeyVerificationSession : public QObject +{ + Q_OBJECT + +public: + enum State { + INCOMING, // There is a request for verification incoming + WAITINGFORREADY, // We sent a request for verification and are waiting for ready + READY, // Either party sent a ready as a response to a request; The user selects a method + WAITINGFORACCEPT, // We sent a start and are waiting for an accept + ACCEPTED, // The other party sent an accept and is waiting for a key + WAITINGFORKEY, // We're waiting for a key + WAITINGFORVERIFICATION, // We're waiting for the *user* to verify the emojis + WAITINGFORMAC, // We're waiting for the mac + CANCELED, // The session has been canceled + DONE, // The verification is done + }; + Q_ENUM(State) + + enum Error { + NONE, + TIMEOUT, + REMOTE_TIMEOUT, + USER, + REMOTE_USER, + UNEXPECTED_MESSAGE, + REMOTE_UNEXPECTED_MESSAGE, + UNKNOWN_TRANSACTION, + REMOTE_UNKNOWN_TRANSACTION, + UNKNOWN_METHOD, + REMOTE_UNKNOWN_METHOD, + KEY_MISMATCH, + REMOTE_KEY_MISMATCH, + USER_MISMATCH, + REMOTE_USER_MISMATCH, + INVALID_MESSAGE, + REMOTE_INVALID_MESSAGE, + SESSION_ACCEPTED, + REMOTE_SESSION_ACCEPTED, + MISMATCHED_COMMITMENT, + REMOTE_MISMATCHED_COMMITMENT, + MISMATCHED_SAS, + REMOTE_MISMATCHED_SAS, + }; + Q_ENUM(Error); + + //Q_PROPERTY(int timeLeft READ timeLeft NOTIFY timeLeftChanged) + Q_PROPERTY(QString remoteDeviceId MEMBER m_remoteDeviceId CONSTANT) + Q_PROPERTY(QList<QVariantMap> sasEmojis READ sasEmojis NOTIFY sasEmojisChanged) + Q_PROPERTY(State state READ state NOTIFY stateChanged) + Q_PROPERTY(Error error READ error NOTIFY errorChanged) + + KeyVerificationSession(const QString& remoteUserId, const KeyVerificationRequestEvent& event, Connection* connection, bool encrypted, QObject* parent = nullptr); + KeyVerificationSession(const QString& userId, const QString& deviceId, Connection* connection, QObject* parent = nullptr); + ~KeyVerificationSession(); + + int timeLeft() const; + QList<QVariantMap> sasEmojis() const; + State state() const; + + Error error() const; + +public Q_SLOTS: + void sendRequest(); + void sendReady(); + void sendMac(); + void sendStartSas(); + void sendKey(); + void sendDone(); + void cancelVerification(Error error); + +Q_SIGNALS: + + void timeLeftChanged(); + void startReceived(); + void keyReceived(); + void sasEmojisChanged(); + void stateChanged(); + void errorChanged(); + void finished(); + +private: + QString m_remoteUserId; + QString m_remoteDeviceId; + QString m_transactionId; + Connection* m_connection; + OlmSAS* m_sas = nullptr; + int timeleft = 0; + QList<QVariantMap> m_sasEmojis; + bool startSentByUs = false; + State m_state; + Error m_error; + QString m_startEvent; + QString m_commitment; + QString m_language; + int m_timeout; + bool macReceived = false; + bool m_encrypted; + QStringList m_remoteSupportedMethods; + + void handleReady(const KeyVerificationReadyEvent& event); + void handleStart(const KeyVerificationStartEvent& event); + void handleAccept(const KeyVerificationAcceptEvent& event); + void handleKey(const KeyVerificationKeyEvent& event); + void handleMac(const KeyVerificationMacEvent& event); + void handleDone(const KeyVerificationDoneEvent& event); + void handleCancel(const KeyVerificationCancelEvent& event); + void init(); + void setState(State state); + void setError(Error error); + QStringList commonSupportedMethods(const QStringList& remoteSupportedMethods) const; + QString errorToString(Error error) const; + Error stringToError(const QString& error) const; + QStringList m_supportedMethods = { "m.sas.v1"_ls }; + + QByteArray macInfo(bool verifying, const QString& key = "KEY_IDS"_ls); + QString calculateMac(const QString& input, bool verifying, const QString& keyId= "KEY_IDS"_ls); + + std::pair<QString, QString> emojiForCode(int code); +}; + +} diff --git a/res.qrc b/res.qrc new file mode 100644 index 00000000..f6769103 --- /dev/null +++ b/res.qrc @@ -0,0 +1,5 @@ +<!DOCTYPE RCC><RCC version="1.0"> +<qresource prefix="/"> + <file>sas-emoji.json</file> +</qresource> +</RCC> diff --git a/sas-emoji.json b/sas-emoji.json new file mode 100644 index 00000000..06e1e4b3 --- /dev/null +++ b/sas-emoji.json @@ -0,0 +1,2178 @@ +[ + { + "number": 0, + "emoji": "🐶", + "description": "Dog", + "unicode": "U+1F436", + "translated_descriptions": { + "ar": "كَلب", + "bg": "Куче", + "ca": "Gos", + "cs": "Pes", + "de": "Hund", + "eo": "Hundo", + "es": "Perro", + "et": "Koer", + "fi": "Koira", + "fr": "Chien", + "hr": "pas", + "hu": "Kutya", + "it": "Cane", + "ja": "犬", + "nb_NO": "Hund", + "nl": "Hond", + "pt_BR": "Cachorro", + "ru": "Собака", + "si": "බල්ලා", + "sk": "Hlava psa", + "sr": "пас", + "sv": "Hund", + "szl": null, + "tzm": "Aydi", + "uk": "Пес", + "zh_Hans": "狗" + } + }, + { + "number": 1, + "emoji": "🐱", + "description": "Cat", + "unicode": "U+1F431", + "translated_descriptions": { + "ar": "هِرَّة", + "bg": "Котка", + "ca": "Gat", + "cs": "Kočka", + "de": "Katze", + "eo": "Kato", + "es": "Gato", + "et": "Kass", + "fi": "Kissa", + "fr": "Chat", + "hr": "mačka", + "hu": "Macska", + "it": "Gatto", + "ja": "猫", + "nb_NO": "Katt", + "nl": "Kat", + "pt_BR": "Gato", + "ru": "Кошка", + "si": "පූසා", + "sk": "Hlava mačky", + "sr": "мачка", + "sv": "Katt", + "szl": null, + "tzm": "Amuc", + "uk": "Кіт", + "zh_Hans": "猫" + } + }, + { + "number": 2, + "emoji": "🦁", + "description": "Lion", + "unicode": "U+1F981", + "translated_descriptions": { + "ar": "أَسَد", + "bg": "Лъв", + "ca": "Lleó", + "cs": "Lev", + "de": "Löwe", + "eo": "Leono", + "es": "León", + "et": "Lõvi", + "fi": "Leijona", + "fr": "Lion", + "hr": "lav", + "hu": "Oroszlán", + "it": "Leone", + "ja": "ライオン", + "nb_NO": "Løve", + "nl": "Leeuw", + "pt_BR": "Leão", + "ru": "Лев", + "si": "සිංහයා", + "sk": "Hlava leva", + "sr": "лав", + "sv": "Lejon", + "szl": null, + "tzm": "Izem", + "uk": "Лев", + "zh_Hans": "狮子" + } + }, + { + "number": 3, + "emoji": "🐎", + "description": "Horse", + "unicode": "U+1F40E", + "translated_descriptions": { + "ar": "حِصَان", + "bg": "Кон", + "ca": "Cavall", + "cs": "Kůň", + "de": "Pferd", + "eo": "Ĉevalo", + "es": "Caballo", + "et": "Hobune", + "fi": "Hevonen", + "fr": "Cheval", + "hr": "konj", + "hu": "Ló", + "it": "Cavallo", + "ja": "馬", + "nb_NO": "Hest", + "nl": "Paard", + "pt_BR": "Cavalo", + "ru": "Лошадь", + "si": "අශ්වයා", + "sk": "Kôň", + "sr": "коњ", + "sv": "Häst", + "szl": null, + "tzm": "Ayyis", + "uk": "Кінь", + "zh_Hans": "马" + } + }, + { + "number": 4, + "emoji": "🦄", + "description": "Unicorn", + "unicode": "U+1F984", + "translated_descriptions": { + "ar": "حِصَانٌ بِقَرن", + "bg": "Еднорог", + "ca": "Unicorn", + "cs": "Jednorožec", + "de": "Einhorn", + "eo": "Unukorno", + "es": "Unicornio", + "et": "Ükssarvik", + "fi": "Yksisarvinen", + "fr": "Licorne", + "hr": "jednorog", + "hu": "Egyszarvú", + "it": "Unicorno", + "ja": "ユニコーン", + "nb_NO": "Enhjørning", + "nl": "Eenhoorn", + "pt_BR": "Unicórnio", + "ru": "Единорог", + "si": null, + "sk": "Hlava jednorožca", + "sr": "једнорог", + "sv": "Enhörning", + "szl": null, + "tzm": null, + "uk": "Єдиноріг", + "zh_Hans": "独角兽" + } + }, + { + "number": 5, + "emoji": "🐷", + "description": "Pig", + "unicode": "U+1F437", + "translated_descriptions": { + "ar": "خِنزِير", + "bg": "Прасе", + "ca": "Porc", + "cs": "Prase", + "de": "Schwein", + "eo": "Porko", + "es": "Cerdo", + "et": "Siga", + "fi": "Sika", + "fr": "Cochon", + "hr": "svinja", + "hu": "Malac", + "it": "Maiale", + "ja": "ブタ", + "nb_NO": "Gris", + "nl": "Varken", + "pt_BR": "Porco", + "ru": "Свинья", + "si": null, + "sk": "Hlava prasaťa", + "sr": "прасе", + "sv": "Gris", + "szl": null, + "tzm": "Ilef", + "uk": "Свиня", + "zh_Hans": "猪" + } + }, + { + "number": 6, + "emoji": "🐘", + "description": "Elephant", + "unicode": "U+1F418", + "translated_descriptions": { + "ar": "فِيل", + "bg": "Слон", + "ca": "Elefant", + "cs": "Slon", + "de": "Elefant", + "eo": "Elefanto", + "es": "Elefante", + "et": "Elevant", + "fi": "Norsu", + "fr": "Éléphant", + "hr": "slon", + "hu": "Elefánt", + "it": "Elefante", + "ja": "ゾウ", + "nb_NO": "Elefant", + "nl": "Olifant", + "pt_BR": "Elefante", + "ru": "Слон", + "si": null, + "sk": "Slon", + "sr": "слон", + "sv": "Elefant", + "szl": null, + "tzm": "Ilu", + "uk": "Слон", + "zh_Hans": "大象" + } + }, + { + "number": 7, + "emoji": "🐰", + "description": "Rabbit", + "unicode": "U+1F430", + "translated_descriptions": { + "ar": "أَرنَب", + "bg": "Заек", + "ca": "Conill", + "cs": "Králík", + "de": "Hase", + "eo": "Kuniklo", + "es": "Conejo", + "et": "Jänes", + "fi": "Kani", + "fr": "Lapin", + "hr": "zec", + "hu": "Nyúl", + "it": "Coniglio", + "ja": "うさぎ", + "nb_NO": "Kanin", + "nl": "Konijn", + "pt_BR": "Coelho", + "ru": "Кролик", + "si": null, + "sk": "Hlava zajaca", + "sr": "зец", + "sv": "Kanin", + "szl": null, + "tzm": "Agnin", + "uk": "Кріль", + "zh_Hans": "兔子" + } + }, + { + "number": 8, + "emoji": "🐼", + "description": "Panda", + "unicode": "U+1F43C", + "translated_descriptions": { + "ar": "باندَا", + "bg": "Панда", + "ca": "Panda", + "cs": "Panda", + "de": "Panda", + "eo": "Pando", + "es": "Panda", + "et": "Panda", + "fi": "Panda", + "fr": "Panda", + "hr": "panda", + "hu": "Panda", + "it": "Panda", + "ja": "パンダ", + "nb_NO": "Panda", + "nl": "Panda", + "pt_BR": "Panda", + "ru": "Панда", + "si": null, + "sk": "Hlava pandy", + "sr": "панда", + "sv": "Panda", + "szl": null, + "tzm": null, + "uk": "Панда", + "zh_Hans": "熊猫" + } + }, + { + "number": 9, + "emoji": "🐓", + "description": "Rooster", + "unicode": "U+1F413", + "translated_descriptions": { + "ar": "دِيك", + "bg": "Петел", + "ca": "Gall", + "cs": "Kohout", + "de": "Hahn", + "eo": "Virkoko", + "es": "Gallo", + "et": "Kukk", + "fi": "Kukko", + "fr": "Coq", + "hr": "kokot", + "hu": "Kakas", + "it": "Gallo", + "ja": "ニワトリ", + "nb_NO": "Hane", + "nl": "Haan", + "pt_BR": "Galo", + "ru": "Петух", + "si": null, + "sk": "Kohút", + "sr": "петао", + "sv": "Tupp", + "szl": null, + "tzm": "Ayaẓiḍ", + "uk": "Когут", + "zh_Hans": "公鸡" + } + }, + { + "number": 10, + "emoji": "🐧", + "description": "Penguin", + "unicode": "U+1F427", + "translated_descriptions": { + "ar": "بِطريق", + "bg": "Пингвин", + "ca": "Pingüí", + "cs": "Tučňák", + "de": "Pinguin", + "eo": "Pingveno", + "es": "Pingüino", + "et": "Pingviin", + "fi": "Pingviini", + "fr": "Manchot", + "hr": "pingvin", + "hu": "Pingvin", + "it": "Pinguino", + "ja": "ペンギン", + "nb_NO": "Pingvin", + "nl": "Pinguïn", + "pt_BR": "Pinguim", + "ru": "Пингвин", + "si": null, + "sk": "Tučniak", + "sr": "пингвин", + "sv": "Pingvin", + "szl": null, + "tzm": null, + "uk": "Пінгвін", + "zh_Hans": "企鹅" + } + }, + { + "number": 11, + "emoji": "🐢", + "description": "Turtle", + "unicode": "U+1F422", + "translated_descriptions": { + "ar": "سُلحفاة", + "bg": "Костенурка", + "ca": "Tortuga", + "cs": "Želva", + "de": "Schildkröte", + "eo": "Testudo", + "es": "Tortuga", + "et": "Kilpkonn", + "fi": "Kilpikonna", + "fr": "Tortue", + "hr": "kornjača", + "hu": "Teknős", + "it": "Tartaruga", + "ja": "亀", + "nb_NO": "Skilpadde", + "nl": "Schildpad", + "pt_BR": "Tartaruga", + "ru": "Черепаха", + "si": null, + "sk": "Korytnačka", + "sr": "корњача", + "sv": "Sköldpadda", + "szl": null, + "tzm": "Ifker", + "uk": "Черепаха", + "zh_Hans": "乌龟" + } + }, + { + "number": 12, + "emoji": "🐟", + "description": "Fish", + "unicode": "U+1F41F", + "translated_descriptions": { + "ar": "سَمَكَة", + "bg": "Риба", + "ca": "Peix", + "cs": "Ryba", + "de": "Fisch", + "eo": "Fiŝo", + "es": "Pez", + "et": "Kala", + "fi": "Kala", + "fr": "Poisson", + "hr": "riba", + "hu": "Hal", + "it": "Pesce", + "ja": "魚", + "nb_NO": "Fisk", + "nl": "Vis", + "pt_BR": "Peixe", + "ru": "Рыба", + "si": null, + "sk": "Ryba", + "sr": "риба", + "sv": "Fisk", + "szl": null, + "tzm": "Aselm", + "uk": "Риба", + "zh_Hans": "鱼" + } + }, + { + "number": 13, + "emoji": "🐙", + "description": "Octopus", + "unicode": "U+1F419", + "translated_descriptions": { + "ar": "أُخطُبُوط", + "bg": "Октопод", + "ca": "Pop", + "cs": "Chobotnice", + "de": "Oktopus", + "eo": "Polpo", + "es": "Pulpo", + "et": "Kaheksajalg", + "fi": "Tursas", + "fr": "Poulpe", + "hr": "hobotnica", + "hu": "Polip", + "it": "Polpo", + "ja": "たこ", + "nb_NO": "Blekksprut", + "nl": "Octopus", + "pt_BR": "Polvo", + "ru": "Осьминог", + "si": null, + "sk": "Chobotnica", + "sr": "октопод", + "sv": "Bläckfisk", + "szl": null, + "tzm": null, + "uk": "Восьминіг", + "zh_Hans": "章鱼" + } + }, + { + "number": 14, + "emoji": "🦋", + "description": "Butterfly", + "unicode": "U+1F98B", + "translated_descriptions": { + "ar": "فَرَاشَة", + "bg": "Пеперуда", + "ca": "Papallona", + "cs": "Motýl", + "de": "Schmetterling", + "eo": "Papilio", + "es": "Mariposa", + "et": "Liblikas", + "fi": "Perhonen", + "fr": "Papillon", + "hr": "leptir", + "hu": "Pillangó", + "it": "Farfalla", + "ja": "ちょうちょ", + "nb_NO": "Sommerfugl", + "nl": "Vlinder", + "pt_BR": "Borboleta", + "ru": "Бабочка", + "si": null, + "sk": "Motýľ", + "sr": "лептир", + "sv": "Fjäril", + "szl": null, + "tzm": null, + "uk": "Метелик", + "zh_Hans": "蝴蝶" + } + }, + { + "number": 15, + "emoji": "🌷", + "description": "Flower", + "unicode": "U+1F337", + "translated_descriptions": { + "ar": "زَهرَة", + "bg": "Цвете", + "ca": "Flor", + "cs": "Květina", + "de": "Blume", + "eo": "Floro", + "es": "Flor", + "et": "Lill", + "fi": "Kukka", + "fr": "Fleur", + "hr": "svijet", + "hu": "Virág", + "it": "Fiore", + "ja": "花", + "nb_NO": "Blomst", + "nl": "Bloem", + "pt_BR": "Flor", + "ru": "Цветок", + "si": null, + "sk": "Tulipán", + "sr": "цвет", + "sv": "Blomma", + "szl": null, + "tzm": null, + "uk": "Квітка", + "zh_Hans": "花" + } + }, + { + "number": 16, + "emoji": "🌳", + "description": "Tree", + "unicode": "U+1F333", + "translated_descriptions": { + "ar": "شَجَرَة", + "bg": "Дърво", + "ca": "Arbre", + "cs": "Strom", + "de": "Baum", + "eo": "Arbo", + "es": "Árbol", + "et": "Puu", + "fi": "Puu", + "fr": "Arbre", + "hr": "drvo", + "hu": "Fa", + "it": "Albero", + "ja": "木", + "nb_NO": "Tre", + "nl": "Boom", + "pt_BR": "Árvore", + "ru": "Дерево", + "si": null, + "sk": "Listnatý strom", + "sr": "дрво", + "sv": "Träd", + "szl": null, + "tzm": "Aseklu", + "uk": "Дерево", + "zh_Hans": "树" + } + }, + { + "number": 17, + "emoji": "🌵", + "description": "Cactus", + "unicode": "U+1F335", + "translated_descriptions": { + "ar": "صبار", + "bg": "Кактус", + "ca": "Cactus", + "cs": "Kaktus", + "de": "Kaktus", + "eo": "Kakto", + "es": "Cactus", + "et": "Kaktus", + "fi": "Kaktus", + "fr": "Cactus", + "hr": "kaktus", + "hu": "Kaktusz", + "it": "Cactus", + "ja": "サボテン", + "nb_NO": "Kaktus", + "nl": "Cactus", + "pt_BR": "Cacto", + "ru": "Кактус", + "si": null, + "sk": "Kaktus", + "sr": "кактус", + "sv": "Kaktus", + "szl": null, + "tzm": null, + "uk": "Кактус", + "zh_Hans": "仙人掌" + } + }, + { + "number": 18, + "emoji": "🍄", + "description": "Mushroom", + "unicode": "U+1F344", + "translated_descriptions": { + "ar": "فُطر", + "bg": "Гъба", + "ca": "Bolet", + "cs": "Houba", + "de": "Pilz", + "eo": "Fungo", + "es": "Seta", + "et": "Seen", + "fi": "Sieni", + "fr": "Champignon", + "hr": "gljiva", + "hu": "Gomba", + "it": "Fungo", + "ja": "きのこ", + "nb_NO": "Sopp", + "nl": "Paddenstoel", + "pt_BR": "Cogumelo", + "ru": "Гриб", + "si": null, + "sk": "Huba", + "sr": "печурка", + "sv": "Svamp", + "szl": null, + "tzm": "Agursel", + "uk": "Гриб", + "zh_Hans": "蘑菇" + } + }, + { + "number": 19, + "emoji": "🌏", + "description": "Globe", + "unicode": "U+1F30F", + "translated_descriptions": { + "ar": "كُرَةٌ أرضِيَّة", + "bg": "Глобус", + "ca": "Globus terraqüi", + "cs": "Zeměkoule", + "de": "Globus", + "eo": "Globo", + "es": "Globo", + "et": "Maakera", + "fi": "Maapallo", + "fr": "Globe", + "hr": "Globus", + "hu": "Földgömb", + "it": "Globo", + "ja": "地球", + "nb_NO": "Globus", + "nl": "Wereldbol", + "pt_BR": "Globo", + "ru": "Глобус", + "si": null, + "sk": "Zemeguľa", + "sr": "глобус", + "sv": "Jordklot", + "szl": null, + "tzm": null, + "uk": "Глобус", + "zh_Hans": "地球" + } + }, + { + "number": 20, + "emoji": "🌙", + "description": "Moon", + "unicode": "U+1F319", + "translated_descriptions": { + "ar": "قَمَر", + "bg": "Луна", + "ca": "Lluna", + "cs": "Měsíc", + "de": "Mond", + "eo": "Luno", + "es": "Luna", + "et": "Kuu", + "fi": "Kuu", + "fr": "Lune", + "hr": "mjesec", + "hu": "Hold", + "it": "Luna", + "ja": "月", + "nb_NO": "Måne", + "nl": "Maan", + "pt_BR": "Lua", + "ru": "Луна", + "si": null, + "sk": "Polmesiac", + "sr": "месец", + "sv": "Måne", + "szl": null, + "tzm": "Ayyur", + "uk": "Місяць", + "zh_Hans": "月亮" + } + }, + { + "number": 21, + "emoji": "☁️", + "description": "Cloud", + "unicode": "U+2601U+FE0F", + "translated_descriptions": { + "ar": "سَحابَة", + "bg": "Облак", + "ca": "Núvol", + "cs": "Mrak", + "de": "Wolke", + "eo": "Nubo", + "es": "Nube", + "et": "Pilv", + "fi": "Pilvi", + "fr": "Nuage", + "hr": "oblak", + "hu": "Felhő", + "it": "Nuvola", + "ja": "雲", + "nb_NO": "Sky", + "nl": "Wolk", + "pt_BR": "Nuvem", + "ru": "Облако", + "si": null, + "sk": "Oblak", + "sr": "облак", + "sv": "Moln", + "szl": null, + "tzm": null, + "uk": "Хмара", + "zh_Hans": "云" + } + }, + { + "number": 22, + "emoji": "🔥", + "description": "Fire", + "unicode": "U+1F525", + "translated_descriptions": { + "ar": "نار", + "bg": "Огън", + "ca": "Foc", + "cs": "Oheň", + "de": "Feuer", + "eo": "Fajro", + "es": "Fuego", + "et": "Tuli", + "fi": "Tuli", + "fr": "Feu", + "hr": "vatra", + "hu": "Tűz", + "it": "Fuoco", + "ja": "炎", + "nb_NO": "Flamme", + "nl": "Vuur", + "pt_BR": "Fogo", + "ru": "Огонь", + "si": null, + "sk": "Oheň", + "sr": "ватра", + "sv": "Eld", + "szl": null, + "tzm": "Timessi", + "uk": "Вогонь", + "zh_Hans": "火" + } + }, + { + "number": 23, + "emoji": "🍌", + "description": "Banana", + "unicode": "U+1F34C", + "translated_descriptions": { + "ar": "مَوزَة", + "bg": "Банан", + "ca": "Plàtan", + "cs": "Banán", + "de": "Banane", + "eo": "Banano", + "es": "Plátano", + "et": "Banaan", + "fi": "Banaani", + "fr": "Banane", + "hr": "banana", + "hu": "Banán", + "it": "Banana", + "ja": "バナナ", + "nb_NO": "Banan", + "nl": "Banaan", + "pt_BR": "Banana", + "ru": "Банан", + "si": null, + "sk": "Banán", + "sr": "банана", + "sv": "Banan", + "szl": null, + "tzm": "Tabanant", + "uk": "Банан", + "zh_Hans": "香蕉" + } + }, + { + "number": 24, + "emoji": "🍎", + "description": "Apple", + "unicode": "U+1F34E", + "translated_descriptions": { + "ar": "تُفَّاحَة", + "bg": "Ябълка", + "ca": "Poma", + "cs": "Jablko", + "de": "Apfel", + "eo": "Pomo", + "es": "Manzana", + "et": "Õun", + "fi": "Omena", + "fr": "Pomme", + "hr": "jabuka", + "hu": "Alma", + "it": "Mela", + "ja": "リンゴ", + "nb_NO": "Eple", + "nl": "Appel", + "pt_BR": "Maçã", + "ru": "Яблоко", + "si": null, + "sk": "Červené jablko", + "sr": "јабука", + "sv": "Äpple", + "szl": null, + "tzm": "Tadeffuyt", + "uk": "Яблуко", + "zh_Hans": "苹果" + } + }, + { + "number": 25, + "emoji": "🍓", + "description": "Strawberry", + "unicode": "U+1F353", + "translated_descriptions": { + "ar": "فَراوِلَة", + "bg": "Ягода", + "ca": "Maduixa", + "cs": "Jahoda", + "de": "Erdbeere", + "eo": "Frago", + "es": "Fresa", + "et": "Maasikas", + "fi": "Mansikka", + "fr": "Fraise", + "hr": "jagoda", + "hu": "Eper", + "it": "Fragola", + "ja": "いちご", + "nb_NO": "Jordbær", + "nl": "Aardbei", + "pt_BR": "Morango", + "ru": "Клубника", + "si": null, + "sk": "Jahoda", + "sr": "јагода", + "sv": "Jordgubbe", + "szl": null, + "tzm": null, + "uk": "Полуниця", + "zh_Hans": "草莓" + } + }, + { + "number": 26, + "emoji": "🌽", + "description": "Corn", + "unicode": "U+1F33D", + "translated_descriptions": { + "ar": "ذُرَة", + "bg": "Царевица", + "ca": "Blat de moro", + "cs": "Kukuřice", + "de": "Mais", + "eo": "Maizo", + "es": "Maíz", + "et": "Mais", + "fi": "Maissi", + "fr": "Maïs", + "hr": "kukuruza", + "hu": "Kukorica", + "it": "Mais", + "ja": "とうもろこし", + "nb_NO": "Mais", + "nl": "Maïs", + "pt_BR": "Milho", + "ru": "Кукуруза", + "si": null, + "sk": "Kukuričný klas", + "sr": "кукуруз", + "sv": "Majs", + "szl": null, + "tzm": null, + "uk": "Кукурудза", + "zh_Hans": "玉米" + } + }, + { + "number": 27, + "emoji": "🍕", + "description": "Pizza", + "unicode": "U+1F355", + "translated_descriptions": { + "ar": "بِيتزا", + "bg": "Пица", + "ca": "Pizza", + "cs": "Pizza", + "de": "Pizza", + "eo": "Pico", + "es": "Pizza", + "et": "Pitsa", + "fi": "Pizza", + "fr": "Pizza", + "hr": "pizza", + "hu": "Pizza", + "it": "Pizza", + "ja": "ピザ", + "nb_NO": "Pizza", + "nl": "Pizza", + "pt_BR": "Pizza", + "ru": "Пицца", + "si": null, + "sk": "Pizza", + "sr": "пица", + "sv": "Pizza", + "szl": null, + "tzm": null, + "uk": "Піца", + "zh_Hans": "披萨" + } + }, + { + "number": 28, + "emoji": "🎂", + "description": "Cake", + "unicode": "U+1F382", + "translated_descriptions": { + "ar": "كَعكَة", + "bg": "Торта", + "ca": "Pastís", + "cs": "Dort", + "de": "Kuchen", + "eo": "Torto", + "es": "Tarta", + "et": "Kook", + "fi": "Kakku", + "fr": "Gâteau", + "hr": "torta", + "hu": "Süti", + "it": "Torta", + "ja": "ケーキ", + "nb_NO": "Kake", + "nl": "Taart", + "pt_BR": "Bolo", + "ru": "Торт", + "si": null, + "sk": "Narodeninová torta", + "sr": "торта", + "sv": "Tårta", + "szl": null, + "tzm": null, + "uk": "Пиріг", + "zh_Hans": "蛋糕" + } + }, + { + "number": 29, + "emoji": "❤️", + "description": "Heart", + "unicode": "U+2764U+FE0F", + "translated_descriptions": { + "ar": "قَلب", + "bg": "Сърце", + "ca": "Cor", + "cs": "Srdce", + "de": "Herz", + "eo": "Koro", + "es": "Corazón", + "et": "Süda", + "fi": "Sydän", + "fr": "Cœur", + "hr": "srca", + "hu": "Szív", + "it": "Cuore", + "ja": "ハート", + "nb_NO": "Hjerte", + "nl": "Hart", + "pt_BR": "Coração", + "ru": "Сердце", + "si": null, + "sk": "červené srdce", + "sr": "срце", + "sv": "Hjärta", + "szl": null, + "tzm": "Ul", + "uk": "Серце", + "zh_Hans": "心" + } + }, + { + "number": 30, + "emoji": "😀", + "description": "Smiley", + "unicode": "U+1F600", + "translated_descriptions": { + "ar": "اِبتِسَامَة", + "bg": "Усмивка", + "ca": "Somrient", + "cs": "Smajlík", + "de": "Lächeln", + "eo": "Rideto", + "es": "Emoticono", + "et": "Smaili", + "fi": "Hymynaama", + "fr": "Sourire", + "hr": "smajlića", + "hu": "Mosoly", + "it": "Faccina sorridente", + "ja": "スマイル", + "nb_NO": "Smilefjes", + "nl": "Smiley", + "pt_BR": "Sorriso", + "ru": "Улыбка", + "si": null, + "sk": "Škeriaca sa tvár", + "sr": "смајли", + "sv": "Smiley", + "szl": null, + "tzm": null, + "uk": "Посмішка", + "zh_Hans": "笑脸" + } + }, + { + "number": 31, + "emoji": "🤖", + "description": "Robot", + "unicode": "U+1F916", + "translated_descriptions": { + "ar": "رُوبُوت", + "bg": "Робот", + "ca": "Robot", + "cs": "Robot", + "de": "Roboter", + "eo": "Roboto", + "es": "Robot", + "et": "Robot", + "fi": "Robotti", + "fr": "Robot", + "hr": "robot", + "hu": "Robot", + "it": "Robot", + "ja": "ロボと", + "nb_NO": "Robot", + "nl": "Robot", + "pt_BR": "Robô", + "ru": "Робот", + "si": null, + "sk": "Robot", + "sr": "робот", + "sv": "Robot", + "szl": null, + "tzm": "Aṛubu", + "uk": "Робот", + "zh_Hans": "机器人" + } + }, + { + "number": 32, + "emoji": "🎩", + "description": "Hat", + "unicode": "U+1F3A9", + "translated_descriptions": { + "ar": "قُبَّعَة", + "bg": "Шапка", + "ca": "Barret", + "cs": "Klobouk", + "de": "Hut", + "eo": "Ĉapelo", + "es": "Sombrero", + "et": "Kübar", + "fi": "Hattu", + "fr": "Chapeau", + "hr": "kapa", + "hu": "Kalap", + "it": "Cappello", + "ja": "帽子", + "nb_NO": "Hatt", + "nl": "Hoed", + "pt_BR": "Chapéu", + "ru": "Шляпа", + "si": null, + "sk": "Cilinder", + "sr": "шешир", + "sv": "Hatt", + "szl": null, + "tzm": "Taraza", + "uk": "Капелюх", + "zh_Hans": "帽子" + } + }, + { + "number": 33, + "emoji": "👓", + "description": "Glasses", + "unicode": "U+1F453", + "translated_descriptions": { + "ar": "نَظَّارَة", + "bg": "Очила", + "ca": "Ulleres", + "cs": "Brýle", + "de": "Brille", + "eo": "Okulvitroj", + "es": "Gafas", + "et": "Prillid", + "fi": "Silmälasit", + "fr": "Lunettes", + "hr": "naočale", + "hu": "Szemüveg", + "it": "Occhiali", + "ja": "めがね", + "nb_NO": "Briller", + "nl": "Bril", + "pt_BR": "Óculos", + "ru": "Очки", + "si": null, + "sk": "Okuliare", + "sr": "наочаре", + "sv": "Glasögon", + "szl": null, + "tzm": null, + "uk": "Окуляри", + "zh_Hans": "眼镜" + } + }, + { + "number": 34, + "emoji": "🔧", + "description": "Spanner", + "unicode": "U+1F527", + "translated_descriptions": { + "ar": "مِفتَاحُ رَبط", + "bg": "Гаечен ключ", + "ca": "Clau anglesa", + "cs": "Klíč", + "de": "Schraubenschlüssel", + "eo": "Ŝraŭbŝlosilo", + "es": "Llave inglesa", + "et": "Mutrivõti", + "fi": "Kiintoavain", + "fr": "Clé à molette", + "hr": "ključ", + "hu": "Csavarkulcs", + "it": "Chiave inglese", + "ja": "スパナ", + "nb_NO": "Fastnøkkel", + "nl": "Moersleutel", + "pt_BR": "Chave inglesa", + "ru": "Ключ", + "si": null, + "sk": "Francúzsky kľúč", + "sr": "кључ", + "sv": "Skruvnyckel", + "szl": null, + "tzm": null, + "uk": "Гайковий ключ", + "zh_Hans": "扳手" + } + }, + { + "number": 35, + "emoji": "🎅", + "description": "Santa", + "unicode": "U+1F385", + "translated_descriptions": { + "ar": "سانتا", + "bg": "Дядо Коледа", + "ca": "Pare Noél", + "cs": "Mikuláš", + "de": "Weihnachtsmann", + "eo": "Kristnaska viro", + "es": "Papá Noel", + "et": "Jõuluvana", + "fi": "Joulupukki", + "fr": "Père Noël", + "hr": "deda Mraz", + "hu": "Télapó", + "it": "Babbo Natale", + "ja": "サンタ", + "nb_NO": "Julenisse", + "nl": "Kerstman", + "pt_BR": "Papai-noel", + "ru": "Санта", + "si": null, + "sk": "Santa Claus", + "sr": "деда Мраз", + "sv": "Tomte", + "szl": null, + "tzm": null, + "uk": "Санта Клаус", + "zh_Hans": "圣诞老人" + } + }, + { + "number": 36, + "emoji": "👍", + "description": "Thumbs Up", + "unicode": "U+1F44D", + "translated_descriptions": { + "ar": "رَفعُ إِبهَام", + "bg": "Палец нагоре", + "ca": "Polzes amunt", + "cs": "Palec nahoru", + "de": "Daumen Hoch", + "eo": "Dikfingro supren", + "es": "Pulgar arriba", + "et": "Pöidlad püsti", + "fi": "Peukalo ylös", + "fr": "Pouce en l’air", + "hr": "palac gore", + "hu": "Hüvelykujj fel", + "it": "Pollice alzato", + "ja": "いいね", + "nb_NO": "Tommel Opp", + "nl": "Duim omhoog", + "pt_BR": "Joinha", + "ru": "Большой палец вверх", + "si": null, + "sk": "Palec nahor", + "sr": "палчић горе", + "sv": "Tummen upp", + "szl": null, + "tzm": null, + "uk": "Великий палець вгору", + "zh_Hans": "赞" + } + }, + { + "number": 37, + "emoji": "☂️", + "description": "Umbrella", + "unicode": "U+2602U+FE0F", + "translated_descriptions": { + "ar": "مِظَلَّة", + "bg": "Чадър", + "ca": "Paraigües", + "cs": "Deštník", + "de": "Regenschirm", + "eo": "Ombrelo", + "es": "Paraguas", + "et": "Vihmavari", + "fi": "Sateenvarjo", + "fr": "Parapluie", + "hr": "kišobran", + "hu": "Esernyő", + "it": "Ombrello", + "ja": "傘", + "nb_NO": "Paraply", + "nl": "Paraplu", + "pt_BR": "Guarda-chuva", + "ru": "Зонт", + "si": null, + "sk": "Dáždnik", + "sr": "кишобран", + "sv": "Paraply", + "szl": null, + "tzm": null, + "uk": "Парасолька", + "zh_Hans": "伞" + } + }, + { + "number": 38, + "emoji": "⌛", + "description": "Hourglass", + "unicode": "U+231B", + "translated_descriptions": { + "ar": "سَاعَةٌ رَملِيَّة", + "bg": "Пясъчен часовник", + "ca": "Rellotge de sorra", + "cs": "Přesýpací hodiny", + "de": "Sanduhr", + "eo": "Sablohorloĝo", + "es": "Reloj de arena", + "et": "Liivakell", + "fi": "Tiimalasi", + "fr": "Sablier", + "hr": "pješčani sat", + "hu": "Homokóra", + "it": "Clessidra", + "ja": "砂時計", + "nb_NO": "Timeglass", + "nl": "Zandloper", + "pt_BR": "Ampulheta", + "ru": "Песочные часы", + "si": null, + "sk": "Presýpacie hodiny", + "sr": "пешчаник", + "sv": "Timglas", + "szl": null, + "tzm": null, + "uk": "Пісковий годинник", + "zh_Hans": "沙漏" + } + }, + { + "number": 39, + "emoji": "⏰", + "description": "Clock", + "unicode": "U+23F0", + "translated_descriptions": { + "ar": "سَاعَة", + "bg": "Часовник", + "ca": "Rellotge", + "cs": "Hodiny", + "de": "Uhr", + "eo": "Horloĝo", + "es": "Reloj", + "et": "Kell", + "fi": "Pöytäkello", + "fr": "Réveil", + "hr": "sat", + "hu": "Óra", + "it": "Orologio", + "ja": "時計", + "nb_NO": "Klokke", + "nl": "Wekker", + "pt_BR": "Relógio", + "ru": "Часы", + "si": null, + "sk": "Budík", + "sr": "сат", + "sv": "Klocka", + "szl": null, + "tzm": null, + "uk": "Годинник", + "zh_Hans": "时钟" + } + }, + { + "number": 40, + "emoji": "🎁", + "description": "Gift", + "unicode": "U+1F381", + "translated_descriptions": { + "ar": "هَدِيَّة", + "bg": "Подарък", + "ca": "Regal", + "cs": "Dárek", + "de": "Geschenk", + "eo": "Donaco", + "es": "Regalo", + "et": "Kingitus", + "fi": "Lahja", + "fr": "Cadeau", + "hr": "poklon", + "hu": "Ajándék", + "it": "Regalo", + "ja": "ギフト", + "nb_NO": "Gave", + "nl": "Geschenk", + "pt_BR": "Presente", + "ru": "Подарок", + "si": null, + "sk": "Zabalený darček", + "sr": "поклон", + "sv": "Present", + "szl": null, + "tzm": null, + "uk": "Подарунок", + "zh_Hans": "礼物" + } + }, + { + "number": 41, + "emoji": "💡", + "description": "Light Bulb", + "unicode": "U+1F4A1", + "translated_descriptions": { + "ar": "مِصبَاح", + "bg": "Лампа", + "ca": "Bombeta", + "cs": "Žárovka", + "de": "Glühbirne", + "eo": "Lampo", + "es": "Bombilla", + "et": "Lambipirn", + "fi": "Hehkulamppu", + "fr": "Ampoule", + "hr": "žarulja", + "hu": "Égő", + "it": "Lampadina", + "ja": "電球", + "nb_NO": "Lyspære", + "nl": "Gloeilamp", + "pt_BR": "Lâmpada", + "ru": "Лампочка", + "si": null, + "sk": "Žiarovka", + "sr": "сијалица", + "sv": "Lampa", + "szl": null, + "tzm": null, + "uk": "Лампочка", + "zh_Hans": "灯泡" + } + }, + { + "number": 42, + "emoji": "📕", + "description": "Book", + "unicode": "U+1F4D5", + "translated_descriptions": { + "ar": "كِتَاب", + "bg": "Книга", + "ca": "Llibre", + "cs": "Kniha", + "de": "Buch", + "eo": "Libro", + "es": "Libro", + "et": "Raamat", + "fi": "Kirja", + "fr": "Livre", + "hr": "knjiga", + "hu": "Könyv", + "it": "Libro", + "ja": "本", + "nb_NO": "Bok", + "nl": "Boek", + "pt_BR": "Livro", + "ru": "Книга", + "si": null, + "sk": "Zatvorená kniha", + "sr": "књига", + "sv": "Bok", + "szl": null, + "tzm": "Adlis", + "uk": "Книга", + "zh_Hans": "书" + } + }, + { + "number": 43, + "emoji": "✏️", + "description": "Pencil", + "unicode": "U+270FU+FE0F", + "translated_descriptions": { + "ar": "قَلَمُ رَصاص", + "bg": "Молив", + "ca": "Llapis", + "cs": "Tužka", + "de": "Bleistift", + "eo": "Krajono", + "es": "Lápiz", + "et": "Pliiats", + "fi": "Lyijykynä", + "fr": "Crayon", + "hr": "olovka", + "hu": "Ceruza", + "it": "Matita", + "ja": "鉛筆", + "nb_NO": "Blyant", + "nl": "Potlood", + "pt_BR": "Lápis", + "ru": "Карандаш", + "si": null, + "sk": "Ceruzka", + "sr": "оловка", + "sv": "Penna", + "szl": null, + "tzm": null, + "uk": "Олівець", + "zh_Hans": "铅笔" + } + }, + { + "number": 44, + "emoji": "📎", + "description": "Paperclip", + "unicode": "U+1F4CE", + "translated_descriptions": { + "ar": "مِشبَكُ وَرَق", + "bg": "Кламер", + "ca": "Clip", + "cs": "Sponka", + "de": "Büroklammer", + "eo": "Paperkuntenilo", + "es": "Clip", + "et": "Kirjaklamber", + "fi": "Paperiliitin", + "fr": "Trombone", + "hr": "spajalica", + "hu": "Gémkapocs", + "it": "Graffetta", + "ja": "クリップ", + "nb_NO": "BInders", + "nl": "Papierklemmetje", + "pt_BR": "Clipe de papel", + "ru": "Скрепка", + "si": null, + "sk": "Sponka na papier", + "sr": "спајалица", + "sv": "Gem", + "szl": null, + "tzm": null, + "uk": "Спиначка", + "zh_Hans": "回形针" + } + }, + { + "number": 45, + "emoji": "✂️", + "description": "Scissors", + "unicode": "U+2702U+FE0F", + "translated_descriptions": { + "ar": "مِقَصّ", + "bg": "Ножици", + "ca": "Tisores", + "cs": "Nůžky", + "de": "Schere", + "eo": "Tondilo", + "es": "Tijeras", + "et": "Käärid", + "fi": "Sakset", + "fr": "Ciseaux", + "hr": "škare", + "hu": "Olló", + "it": "Forbici", + "ja": "はさみ", + "nb_NO": "Saks", + "nl": "Schaar", + "pt_BR": "Tesoura", + "ru": "Ножницы", + "si": null, + "sk": "Nožnice", + "sr": "маказе", + "sv": "Sax", + "szl": null, + "tzm": null, + "uk": "Ножиці", + "zh_Hans": "剪刀" + } + }, + { + "number": 46, + "emoji": "🔒", + "description": "Lock", + "unicode": "U+1F512", + "translated_descriptions": { + "ar": "قُفل", + "bg": "Катинар", + "ca": "Cadenat", + "cs": "Zámek", + "de": "Schloss", + "eo": "Seruro", + "es": "Candado", + "et": "Lukk", + "fi": "Lukko", + "fr": "Cadenas", + "hr": "zaključati", + "hu": "Lakat", + "it": "Lucchetto", + "ja": "錠前", + "nb_NO": "Lås", + "nl": "Slot", + "pt_BR": "Cadeado", + "ru": "Замок", + "si": null, + "sk": "Zatvorená zámka", + "sr": "катанац", + "sv": "Lås", + "szl": null, + "tzm": null, + "uk": "Замок", + "zh_Hans": "锁" + } + }, + { + "number": 47, + "emoji": "🔑", + "description": "Key", + "unicode": "U+1F511", + "translated_descriptions": { + "ar": "مِفتَاح", + "bg": "Ключ", + "ca": "Clau", + "cs": "Klíč", + "de": "Schlüssel", + "eo": "Ŝlosilo", + "es": "Llave", + "et": "Võti", + "fi": "Avain", + "fr": "Clé", + "hr": "ključ", + "hu": "Kulcs", + "it": "Chiave", + "ja": "鍵", + "nb_NO": "Nøkkel", + "nl": "Sleutel", + "pt_BR": "Chave", + "ru": "Ключ", + "si": null, + "sk": "Kľúč", + "sr": "кључ", + "sv": "Nyckel", + "szl": null, + "tzm": "Tasarut", + "uk": "Ключ", + "zh_Hans": "钥匙" + } + }, + { + "number": 48, + "emoji": "🔨", + "description": "Hammer", + "unicode": "U+1F528", + "translated_descriptions": { + "ar": "مِطرَقَة", + "bg": "Чук", + "ca": "Martell", + "cs": "Kladivo", + "de": "Hammer", + "eo": "Martelo", + "es": "Martillo", + "et": "Haamer", + "fi": "Vasara", + "fr": "Marteau", + "hr": "čekić", + "hu": "Kalapács", + "it": "Martello", + "ja": "金槌", + "nb_NO": "Hammer", + "nl": "Hamer", + "pt_BR": "Martelo", + "ru": "Молоток", + "si": null, + "sk": "Kladivo", + "sr": "чекић", + "sv": "Hammare", + "szl": null, + "tzm": null, + "uk": "Молоток", + "zh_Hans": "锤子" + } + }, + { + "number": 49, + "emoji": "☎️", + "description": "Telephone", + "unicode": "U+260EU+FE0F", + "translated_descriptions": { + "ar": "تِلِفُون", + "bg": "Телефон", + "ca": "Telèfon", + "cs": "Telefon", + "de": "Telefon", + "eo": "Telefono", + "es": "Telefono", + "et": "Telefon", + "fi": "Puhelin", + "fr": "Téléphone", + "hr": "telefon", + "hu": "Telefon", + "it": "Telefono", + "ja": "電話機", + "nb_NO": "Telefon", + "nl": "Telefoon", + "pt_BR": "Telefone", + "ru": "Телефон", + "si": null, + "sk": "Telefón", + "sr": "телефон", + "sv": "Telefon", + "szl": null, + "tzm": "Atilifun", + "uk": "Телефон", + "zh_Hans": "电话" + } + }, + { + "number": 50, + "emoji": "🏁", + "description": "Flag", + "unicode": "U+1F3C1", + "translated_descriptions": { + "ar": "عَلَم", + "bg": "Флаг", + "ca": "Bandera", + "cs": "Vlajka", + "de": "Flagge", + "eo": "Flago", + "es": "Bandera", + "et": "Lipp", + "fi": "Lippu", + "fr": "Drapeau", + "hr": "zastava", + "hu": "Zászló", + "it": "Bandiera", + "ja": "旗", + "nb_NO": "Flagg", + "nl": "Vlag", + "pt_BR": "Bandeira", + "ru": "Флаг", + "si": null, + "sk": "Kockovaná zástava", + "sr": "застава", + "sv": "Flagga", + "szl": null, + "tzm": "Acenyal", + "uk": "Прапор", + "zh_Hans": "旗帜" + } + }, + { + "number": 51, + "emoji": "🚂", + "description": "Train", + "unicode": "U+1F682", + "translated_descriptions": { + "ar": "قِطَار", + "bg": "Влак", + "ca": "Tren", + "cs": "Vlak", + "de": "Zug", + "eo": "Vagonaro", + "es": "Tren", + "et": "Rong", + "fi": "Juna", + "fr": "Train", + "hr": "vlak", + "hu": "Vonat", + "it": "Treno", + "ja": "電車", + "nb_NO": "Tog", + "nl": "Trein", + "pt_BR": "Trem", + "ru": "Поезд", + "si": null, + "sk": "Rušeň", + "sr": "воз", + "sv": "Tåg", + "szl": null, + "tzm": null, + "uk": "Потяг", + "zh_Hans": "火车" + } + }, + { + "number": 52, + "emoji": "🚲", + "description": "Bicycle", + "unicode": "U+1F6B2", + "translated_descriptions": { + "ar": "دَرّاجَة", + "bg": "Колело", + "ca": "Bicicleta", + "cs": "Kolo", + "de": "Fahrrad", + "eo": "Biciklo", + "es": "Bicicleta", + "et": "Jalgratas", + "fi": "Polkupyörä", + "fr": "Vélo", + "hr": "bicikl", + "hu": "Kerékpár", + "it": "Bicicletta", + "ja": "自転車", + "nb_NO": "Sykkel", + "nl": "Fiets", + "pt_BR": "Bicicleta", + "ru": "Велосипед", + "si": null, + "sk": "Bicykel", + "sr": "бицикл", + "sv": "Cykel", + "szl": null, + "tzm": null, + "uk": "Велосипед", + "zh_Hans": "自行车" + } + }, + { + "number": 53, + "emoji": "✈️", + "description": "Aeroplane", + "unicode": "U+2708U+FE0F", + "translated_descriptions": { + "ar": "طَائِرة", + "bg": "Самолет", + "ca": "Avió", + "cs": "Letadlo", + "de": "Flugzeug", + "eo": "Aviadilo", + "es": "Avión", + "et": "Lennuk", + "fi": "Lentokone", + "fr": "Avion", + "hr": "avion", + "hu": "Repülő", + "it": "Aeroplano", + "ja": "飛行機", + "nb_NO": "Fly", + "nl": "Vliegtuig", + "pt_BR": "Avião", + "ru": "Самолет", + "si": null, + "sk": "Lietadlo", + "sr": "авион", + "sv": "Flygplan", + "szl": null, + "tzm": null, + "uk": "Літак", + "zh_Hans": "飞机" + } + }, + { + "number": 54, + "emoji": "🚀", + "description": "Rocket", + "unicode": "U+1F680", + "translated_descriptions": { + "ar": "صَارُوخ", + "bg": "Ракета", + "ca": "Coet", + "cs": "Raketa", + "de": "Rakete", + "eo": "Raketo", + "es": "Cohete", + "et": "Rakett", + "fi": "Raketti", + "fr": "Fusée", + "hr": "raketa", + "hu": "Rakáta", + "it": "Razzo", + "ja": "ロケット", + "nb_NO": "Rakett", + "nl": "Raket", + "pt_BR": "Foguete", + "ru": "Ракета", + "si": null, + "sk": "Raketa", + "sr": "ракета", + "sv": "Raket", + "szl": null, + "tzm": null, + "uk": "Ракета", + "zh_Hans": "火箭" + } + }, + { + "number": 55, + "emoji": "🏆", + "description": "Trophy", + "unicode": "U+1F3C6", + "translated_descriptions": { + "ar": "كَأسُ النَّصر", + "bg": "Трофей", + "ca": "Trofeu", + "cs": "Pohár", + "de": "Pokal", + "eo": "Trofeo", + "es": "Trofeo", + "et": "Auhind", + "fi": "Palkinto", + "fr": "Trophée", + "hr": "trofej", + "hu": "Trófea", + "it": "Trofeo", + "ja": "トロフィー", + "nb_NO": "Pokal", + "nl": "Trofee", + "pt_BR": "Troféu", + "ru": "Кубок", + "si": null, + "sk": "Trofej", + "sr": "пехар", + "sv": "Trofé", + "szl": null, + "tzm": null, + "uk": "Приз", + "zh_Hans": "奖杯" + } + }, + { + "number": 56, + "emoji": "⚽", + "description": "Ball", + "unicode": "U+26BD", + "translated_descriptions": { + "ar": "كُرَة", + "bg": "Топка", + "ca": "Pilota", + "cs": "Míč", + "de": "Ball", + "eo": "Pilko", + "es": "Bola", + "et": "Pall", + "fi": "Pallo", + "fr": "Ballon", + "hr": "lopta", + "hu": "Labda", + "it": "Palla", + "ja": "ボール", + "nb_NO": "Ball", + "nl": "Bal", + "pt_BR": "Bola", + "ru": "Мяч", + "si": null, + "sk": "Futbal", + "sr": "лопта", + "sv": "Boll", + "szl": null, + "tzm": "Tcama", + "uk": "М'яч", + "zh_Hans": "球" + } + }, + { + "number": 57, + "emoji": "🎸", + "description": "Guitar", + "unicode": "U+1F3B8", + "translated_descriptions": { + "ar": "غيتار", + "bg": "Китара", + "ca": "Guitarra", + "cs": "Kytara", + "de": "Gitarre", + "eo": "Gitaro", + "es": "Guitarra", + "et": "Kitarr", + "fi": "Kitara", + "fr": "Guitare", + "hr": "gitara", + "hu": "Gitár", + "it": "Chitarra", + "ja": "ギター", + "nb_NO": "Gitar", + "nl": "Gitaar", + "pt_BR": "Guitarra", + "ru": "Гитара", + "si": null, + "sk": "Gitara", + "sr": "гитара", + "sv": "Gitarr", + "szl": null, + "tzm": "Agiṭaṛ", + "uk": "Гітара", + "zh_Hans": "吉他" + } + }, + { + "number": 58, + "emoji": "🎺", + "description": "Trumpet", + "unicode": "U+1F3BA", + "translated_descriptions": { + "ar": "بُوق", + "bg": "Тромпет", + "ca": "Trompeta", + "cs": "Trumpeta", + "de": "Trompete", + "eo": "Trumpeto", + "es": "Trompeta", + "et": "Trompet", + "fi": "Trumpetti", + "fr": "Trompette", + "hr": "truba", + "hu": "Trombita", + "it": "Trombetta", + "ja": "トランペット", + "nb_NO": "Trompet", + "nl": "Trompet", + "pt_BR": "Trombeta", + "ru": "Труба", + "si": null, + "sk": "Trúbka", + "sr": "труба", + "sv": "Trumpet", + "szl": null, + "tzm": null, + "uk": "Труба", + "zh_Hans": "喇叭" + } + }, + { + "number": 59, + "emoji": "🔔", + "description": "Bell", + "unicode": "U+1F514", + "translated_descriptions": { + "ar": "جَرَس", + "bg": "Звънец", + "ca": "Campana", + "cs": "Zvonek", + "de": "Glocke", + "eo": "Sonorilo", + "es": "Campana", + "et": "Kelluke", + "fi": "Soittokello", + "fr": "Cloche", + "hr": "zvono", + "hu": "Harang", + "it": "Campana", + "ja": "ベル", + "nb_NO": "Bjelle", + "nl": "Bel", + "pt_BR": "Sino", + "ru": "Колокол", + "si": null, + "sk": "Zvon", + "sr": "звоно", + "sv": "Bjällra", + "szl": null, + "tzm": null, + "uk": "Дзвін", + "zh_Hans": "铃铛" + } + }, + { + "number": 60, + "emoji": "⚓", + "description": "Anchor", + "unicode": "U+2693", + "translated_descriptions": { + "ar": "مِرسَاة", + "bg": "Котва", + "ca": "Àncora", + "cs": "Kotva", + "de": "Anker", + "eo": "Ankro", + "es": "Ancla", + "et": "Ankur", + "fi": "Ankkuri", + "fr": "Ancre", + "hr": "sidro", + "hu": "Horgony", + "it": "Ancora", + "ja": "いかり", + "nb_NO": "Anker", + "nl": "Anker", + "pt_BR": "Âncora", + "ru": "Якорь", + "si": null, + "sk": "Kotva", + "sr": "сидро", + "sv": "Ankare", + "szl": null, + "tzm": null, + "uk": "Якір", + "zh_Hans": "锚" + } + }, + { + "number": 61, + "emoji": "🎧", + "description": "Headphones", + "unicode": "U+1F3A7", + "translated_descriptions": { + "ar": "سَمّاعَة رَأس", + "bg": "Слушалки", + "ca": "Auriculars", + "cs": "Sluchátka", + "de": "Kopfhörer", + "eo": "Kapaŭdilo", + "es": "Cascos", + "et": "Kõrvaklapid", + "fi": "Kuulokkeet", + "fr": "Casque audio", + "hr": "slušalice", + "hu": "Fejhallgató", + "it": "Cuffie", + "ja": "ヘッドホン", + "nb_NO": "Hodetelefoner", + "nl": "Koptelefoon", + "pt_BR": "Fones de ouvido", + "ru": "Наушники", + "si": null, + "sk": "Slúchadlá", + "sr": "слушалице", + "sv": "Hörlurar", + "szl": null, + "tzm": null, + "uk": "Навушники", + "zh_Hans": "耳机" + } + }, + { + "number": 62, + "emoji": "📁", + "description": "Folder", + "unicode": "U+1F4C1", + "translated_descriptions": { + "ar": "مُجَلَّد", + "bg": "Папка", + "ca": "Carpeta", + "cs": "Složka", + "de": "Ordner", + "eo": "Dosierujo", + "es": "Carpeta", + "et": "Kaust", + "fi": "Kansio", + "fr": "Dossier", + "hr": "mapu", + "hu": "Mappa", + "it": "Cartella", + "ja": "フォルダ", + "nb_NO": "Mappe", + "nl": "Map", + "pt_BR": "Pasta", + "ru": "Папка", + "si": null, + "sk": "Fascikel", + "sr": "фасцикла", + "sv": "Mapp", + "szl": null, + "tzm": "Asdaw", + "uk": "Тека", + "zh_Hans": "文件夹" + } + }, + { + "number": 63, + "emoji": "📌", + "description": "Pin", + "unicode": "U+1F4CC", + "translated_descriptions": { + "ar": "دَبُّوس", + "bg": "Кабърче", + "ca": "Xinxeta", + "cs": "Špendlík", + "de": "Stecknadel", + "eo": "Pinglo", + "es": "Alfiler", + "et": "Nööpnõel", + "fi": "Nuppineula", + "fr": "Punaise", + "hr": "pribadača", + "hu": "Rajszeg", + "it": "Puntina", + "ja": "ピン", + "nb_NO": "Tegnestift", + "nl": "Duimspijker", + "pt_BR": "Alfinete", + "ru": "Булавка", + "si": null, + "sk": "Špendlík", + "sr": "чиода", + "sv": "Häftstift", + "szl": null, + "tzm": null, + "uk": "Кнопка", + "zh_Hans": "图钉" + } + } +] |