From 8cacac12288d22a7c81058b1b765bd24e56612f8 Mon Sep 17 00:00:00 2001 From: saturneric Date: Sun, 20 Sep 2026 22:21:55 +0200 Subject: feat(pgp): add OpenPGP structure inspector - add sandbox-compatible module for inspecting packet structure - render armor, cleartext, packet fields, errors, and searchable trees - parse inspector JSON with nested packet support and summaries - add model tests covering parsing, structure detection, and formatting - add module metadata, build integration, and translation catalogs --- src/CMakeLists.txt | 4 + src/m_pgp_inspect/CMakeLists.txt | 41 ++++ src/m_pgp_inspect/PGPInspectDialog.cpp | 237 ++++++++++++++++++++ src/m_pgp_inspect/PGPInspectDialog.h | 88 ++++++++ src/m_pgp_inspect/PGPInspectModel.cpp | 191 ++++++++++++++++ src/m_pgp_inspect/PGPInspectModel.h | 141 ++++++++++++ src/m_pgp_inspect/PGPInspectModule.cpp | 132 +++++++++++ src/m_pgp_inspect/module.json | 15 ++ src/m_pgp_inspect/test/CMakeLists.txt | 53 +++++ src/m_pgp_inspect/test/PGPInspectModelTest.cpp | 298 +++++++++++++++++++++++++ src/m_pgp_inspect/ts/ModulePGPInspect.de_DE.ts | 105 +++++++++ src/m_pgp_inspect/ts/ModulePGPInspect.en_US.ts | 105 +++++++++ src/m_pgp_inspect/ts/ModulePGPInspect.es_ES.ts | 105 +++++++++ src/m_pgp_inspect/ts/ModulePGPInspect.fr_FR.ts | 105 +++++++++ src/m_pgp_inspect/ts/ModulePGPInspect.it_IT.ts | 105 +++++++++ src/m_pgp_inspect/ts/ModulePGPInspect.pl_PL.ts | 105 +++++++++ src/m_pgp_inspect/ts/ModulePGPInspect.ru_RU.ts | 105 +++++++++ src/m_pgp_inspect/ts/ModulePGPInspect.zh_CN.ts | 105 +++++++++ src/m_pgp_inspect/ts/ModulePGPInspect.zh_TW.ts | 105 +++++++++ 19 files changed, 2145 insertions(+) create mode 100644 src/m_pgp_inspect/CMakeLists.txt create mode 100644 src/m_pgp_inspect/PGPInspectDialog.cpp create mode 100644 src/m_pgp_inspect/PGPInspectDialog.h create mode 100644 src/m_pgp_inspect/PGPInspectModel.cpp create mode 100644 src/m_pgp_inspect/PGPInspectModel.h create mode 100644 src/m_pgp_inspect/PGPInspectModule.cpp create mode 100644 src/m_pgp_inspect/module.json create mode 100644 src/m_pgp_inspect/test/CMakeLists.txt create mode 100644 src/m_pgp_inspect/test/PGPInspectModelTest.cpp create mode 100644 src/m_pgp_inspect/ts/ModulePGPInspect.de_DE.ts create mode 100644 src/m_pgp_inspect/ts/ModulePGPInspect.en_US.ts create mode 100644 src/m_pgp_inspect/ts/ModulePGPInspect.es_ES.ts create mode 100644 src/m_pgp_inspect/ts/ModulePGPInspect.fr_FR.ts create mode 100644 src/m_pgp_inspect/ts/ModulePGPInspect.it_IT.ts create mode 100644 src/m_pgp_inspect/ts/ModulePGPInspect.pl_PL.ts create mode 100644 src/m_pgp_inspect/ts/ModulePGPInspect.ru_RU.ts create mode 100644 src/m_pgp_inspect/ts/ModulePGPInspect.zh_CN.ts create mode 100644 src/m_pgp_inspect/ts/ModulePGPInspect.zh_TW.ts diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4f6f497..e1e9b4b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -27,6 +27,10 @@ add_subdirectory(m_key_server_sync) add_subdirectory(m_email) +# No network and no GnuPG: an inspector reads bytes and nothing else, so it is +# fine in the sandboxed build too. +add_subdirectory(m_pgp_inspect) + # modules that are not needed in sandboxed environment if(NOT GPGFRONTEND_XCODE_ENABLE_SANDBOX) add_subdirectory(m_ver_check) diff --git a/src/m_pgp_inspect/CMakeLists.txt b/src/m_pgp_inspect/CMakeLists.txt new file mode 100644 index 0000000..705ac23 --- /dev/null +++ b/src/m_pgp_inspect/CMakeLists.txt @@ -0,0 +1,41 @@ +# Copyright (C) 2021-2024 Saturneric +# +# This file is part of GpgFrontend. +# +# GpgFrontend is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# GpgFrontend is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with GpgFrontend. If not, see . +# +# The initial version of the source code is inherited from +# the gpg4usb project, which is under GPL-3.0-or-later. +# +# All the source code of GpgFrontend was modified and released by +# Saturneric starting on May 12, 2021. +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# Everything about this module -- id, version, name, description, author, +# capabilities and translation context -- lives in module.json beside this file. +# +# SOURCES is spelled out rather than left to aux_source_directory: the test +# directory below compiles PGPInspectModel.cpp on its own, and a stray file +# picked up by a glob would quietly break that split. +gf_add_module( + NAME pgp_inspect + QT Core Widgets + SOURCES PGPInspectModule.cpp + PGPInspectModel.cpp + PGPInspectDialog.cpp) + +if(GPGFRONTEND_MODULES_BUILD_TESTS) + add_subdirectory(test) +endif() diff --git a/src/m_pgp_inspect/PGPInspectDialog.cpp b/src/m_pgp_inspect/PGPInspectDialog.cpp new file mode 100644 index 0000000..e8c58cf --- /dev/null +++ b/src/m_pgp_inspect/PGPInspectDialog.cpp @@ -0,0 +1,237 @@ +/** + * Copyright (C) 2021-2024 Saturneric + * + * This file is part of GpgFrontend. + * + * GpgFrontend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GpgFrontend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GpgFrontend. If not, see . + * + * The initial version of the source code is inherited from + * the gpg4usb project, which is under GPL-3.0-or-later. + * + * All the source code of GpgFrontend was modified and released by + * Saturneric starting on May 12, 2021. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#include "PGPInspectDialog.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +/// Which column holds what. Packet rows use the first only; a field row puts +/// its label there and its value beside it. +constexpr int kLabelColumn = 0; +constexpr int kValueColumn = 1; + +auto HumanSize(qint64 bytes) -> QString { return UDUP(GFUIHumanSize(bytes)); } + +} // namespace + +auto PGPInspectCurrentTabBytes() -> QByteArray { + auto* content = GFUITakeCurrentEditorContent(); + if (content == nullptr) return {}; + + const auto* data = static_cast(GFBufferData(content)); + const auto size = static_cast(GFBufferSize(content)); + auto bytes = QByteArray(data == nullptr ? "" : data, size); + GFBufferRelease(content); + return bytes; +} + +auto PGPInspectBytes(const QByteArray& data) -> PGPInspectDocument { + PGPInspectDocument document; + if (data.isEmpty()) return document; + + auto* buffer = + GFBufferNewFromBytes(data.constData(), static_cast(data.size())); + if (buffer == nullptr) return document; + + char* json = nullptr; + const auto status = GFPgpInspectData(buffer, &json); + GFBufferRelease(buffer); + + if (status != 0 || json == nullptr) return document; + return ParsePGPInspectDocument(UDUP(json).toUtf8()); +} + +PGPInspectDialog::PGPInspectDialog(const PGPInspectDocument& document, + qint64 size, QWidget* parent) + : QDialog(parent), document_(document), size_(size) { + setWindowTitle(QCoreApplication::translate("GTrC", "OpenPGP Structure")); + setAttribute(Qt::WA_DeleteOnClose); + resize(820, 620); + create_widgets(); + render_document(); +} + +void PGPInspectDialog::create_widgets() { + search_ = new QLineEdit(this); + search_->setClearButtonEnabled(true); + search_->setPlaceholderText( + QCoreApplication::translate("GTrC", "Search packets, fields and values")); + connect(search_, &QLineEdit::textChanged, this, + &PGPInspectDialog::slot_filter_changed); + + tree_ = new QTreeWidget(this); + tree_->setColumnCount(2); + tree_->setHeaderLabels({QCoreApplication::translate("GTrC", "Structure"), + QCoreApplication::translate("GTrC", "Value")}); + tree_->header()->setSectionResizeMode(kLabelColumn, + QHeaderView::ResizeToContents); + tree_->header()->setSectionResizeMode(kValueColumn, QHeaderView::Stretch); + tree_->setUniformRowHeights(true); + + summary_label_ = new QLabel(this); + summary_label_->setWordWrap(true); + + auto* layout = new QVBoxLayout(); + layout->addWidget(search_); + layout->addWidget(tree_); + layout->addWidget(summary_label_); + setLayout(layout); +} + +void PGPInspectDialog::render_document() { + tree_->clear(); + + if (!document_.valid) { + summary_label_->setText( + document_.parse_error.isEmpty() + ? QCoreApplication::translate( + "GTrC", "The current tab holds no OpenPGP data.") + : document_.parse_error); + return; + } + + int index = 1; + for (const auto& block : document_.blocks) { + auto* item = new QTreeWidgetItem(tree_); + item->setText(kLabelColumn, PGPInspectBlockSummary(block, index++)); + + // The block's own facts are rows like any other, so the search box reaches + // the armor headers and the CRC verdict too. + QVector facts; + if (block.has_armor) { + facts.push_back({QCoreApplication::translate("GTrC", "Armor Type"), + block.armor.block_type}); + facts.push_back( + {QCoreApplication::translate("GTrC", "CRC24"), block.armor.crc24}); + facts += block.armor.headers; + } + if (block.has_cleartext) { + facts.push_back({QCoreApplication::translate("GTrC", "Signed Text"), + HumanSize(block.cleartext_text_size)}); + facts += block.cleartext_headers; + } + if (!block.error.isEmpty()) { + facts.push_back( + {QCoreApplication::translate("GTrC", "Problem"), block.error}); + } + add_fields(item, facts); + + if (block.has_armor && block.armor.crc24 == QLatin1String("mismatch")) { + item->setForeground(kLabelColumn, QBrush(QColor(GFUIDangerColor(this)))); + } else if (!block.error.isEmpty()) { + item->setForeground(kLabelColumn, QBrush(QColor(GFUIWarningColor(this)))); + } + + add_packets(item, block.packets); + } + + tree_->expandAll(); + + // The count goes through a local: a function call inside a translate() + // %n argument makes lupdate silently skip EVERY string in the file. + const int packet_count = document_.PacketCount(); + auto summary = QCoreApplication::translate("GTrC", "%1, %2, %n packet(s)", + nullptr, packet_count) + .arg(document_.format, HumanSize(size_)); + if (!document_.errors.isEmpty()) { + summary += + QLatin1String(" - ") + document_.errors.join(QLatin1String("; ")); + } + summary_label_->setText(summary); +} + +void PGPInspectDialog::add_packets(QTreeWidgetItem* parent, + const QVector& packets) { + int index = 1; + for (const auto& packet : packets) { + auto* item = new QTreeWidgetItem(parent); + item->setText(kLabelColumn, PGPInspectPacketSummary(packet, index++)); + + if (packet.Malformed()) { + item->setForeground(kLabelColumn, QBrush(QColor(GFUIWarningColor(this)))); + } + + auto fields = packet.fields; + if (packet.Malformed()) { + fields.push_back( + {QCoreApplication::translate("GTrC", "Problem"), packet.error}); + } + add_fields(item, fields); + + add_packets(item, packet.children); + } +} + +void PGPInspectDialog::add_fields(QTreeWidgetItem* parent, + const QVector& fields) { + for (const auto& field : fields) { + auto* row = new QTreeWidgetItem(parent); + row->setText(kLabelColumn, field.label); + row->setText(kValueColumn, field.value); + row->setForeground(kLabelColumn, QBrush(QColor(GFUIMutedTextColor(this)))); + } +} + +void PGPInspectDialog::slot_filter_changed(const QString& needle) { + for (int i = 0; i < tree_->topLevelItemCount(); ++i) { + PGPInspectFilterItem(tree_->topLevelItem(i), needle, false); + } + if (!needle.isEmpty()) tree_->expandAll(); +} + +auto PGPInspectFilterItem(QTreeWidgetItem* item, const QString& needle, + bool ancestor_matched) -> bool { + if (item == nullptr) return false; + + const auto matched = needle.isEmpty() || + item->text(0).contains(needle, Qt::CaseInsensitive) || + item->text(1).contains(needle, Qt::CaseInsensitive); + + // A matching packet keeps its whole subtree: the reader searched for the + // packet, so hiding the fields that describe it would answer the wrong + // question. + auto descendant_matched = false; + for (int i = 0; i < item->childCount(); ++i) { + descendant_matched |= PGPInspectFilterItem(item->child(i), needle, + ancestor_matched || matched); + } + + const auto visible = matched || descendant_matched || ancestor_matched; + item->setHidden(!visible); + return visible; +} diff --git a/src/m_pgp_inspect/PGPInspectDialog.h b/src/m_pgp_inspect/PGPInspectDialog.h new file mode 100644 index 0000000..d84b7c9 --- /dev/null +++ b/src/m_pgp_inspect/PGPInspectDialog.h @@ -0,0 +1,88 @@ +/** + * Copyright (C) 2021-2024 Saturneric + * + * This file is part of GpgFrontend. + * + * GpgFrontend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GpgFrontend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GpgFrontend. If not, see . + * + * The initial version of the source code is inherited from + * the gpg4usb project, which is under GPL-3.0-or-later. + * + * All the source code of GpgFrontend was modified and released by + * Saturneric starting on May 12, 2021. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#pragma once + +#include +#include + +#include "PGPInspectModel.h" + +class QLabel; +class QLineEdit; +class QTreeWidget; +class QTreeWidgetItem; + +/// The exact octets of the current editor tab, or empty when there is no tab. +auto PGPInspectCurrentTabBytes() -> QByteArray; + +/// Run the inspector over @p data. Never throws; see PGPInspectDocument::valid. +auto PGPInspectBytes(const QByteArray& data) -> PGPInspectDocument; + +/** + * @brief Shows what the OpenPGP data in the current tab is made of. + * + * A dumb view, and a deliberately bare one: it is handed a document and + * renders it. There is nothing to press -- the content comes from the tab the + * user was already looking at, so a dialog that asked them to fetch it again + * would be asking a question they have already answered. The only control is + * the search box, which is there because a certificate's packet tree is long. + * + * Every rule about what the bytes mean lives in PGPInspectModel.h and, below + * that, in the Rust packet walk. + */ +class PGPInspectDialog : public QDialog { + Q_OBJECT + + public: + PGPInspectDialog(const PGPInspectDocument& document, qint64 size, + QWidget* parent = nullptr); + + private slots: + void slot_filter_changed(const QString& needle); + + private: + void create_widgets(); + void render_document(); + void add_packets(QTreeWidgetItem* parent, + const QVector& packets); + void add_fields(QTreeWidgetItem* parent, + const QVector& fields); + + PGPInspectDocument document_; + qint64 size_ = 0; + + QLineEdit* search_{}; + QTreeWidget* tree_{}; + QLabel* summary_label_{}; +}; + +/// Show a row when it matches, when an ancestor matched, or when a descendant +/// does. Returns whether @p item ended up visible. +auto PGPInspectFilterItem(QTreeWidgetItem* item, const QString& needle, + bool ancestor_matched) -> bool; diff --git a/src/m_pgp_inspect/PGPInspectModel.cpp b/src/m_pgp_inspect/PGPInspectModel.cpp new file mode 100644 index 0000000..874b349 --- /dev/null +++ b/src/m_pgp_inspect/PGPInspectModel.cpp @@ -0,0 +1,191 @@ +/** + * Copyright (C) 2021-2024 Saturneric + * + * This file is part of GpgFrontend. + * + * GpgFrontend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GpgFrontend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GpgFrontend. If not, see . + * + * The initial version of the source code is inherited from + * the gpg4usb project, which is under GPL-3.0-or-later. + * + * All the source code of GpgFrontend was modified and released by + * Saturneric starting on May 12, 2021. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#include "PGPInspectModel.h" + +#include +#include +#include +#include +#include + +namespace { + +auto ParseFields(const QJsonValue& value) -> QVector { + QVector fields; + for (const auto& entry : value.toArray()) { + const auto object = entry.toObject(); + // A row with neither half says nothing; dropping it keeps the detail + // table from growing blank lines out of a malformed document. + if (!object.contains("label") && !object.contains("value")) continue; + fields.push_back( + {object.value("label").toString(), object.value("value").toString()}); + } + return fields; +} + +auto ParsePacket(const QJsonObject& object) -> PGPInspectPacket { + PGPInspectPacket packet; + packet.tag = object.value("tag").toInt(-1); + packet.tag_name = object.value("tagName").toString(); + packet.offset = static_cast(object.value("offset").toDouble()); + packet.header_version = object.value("headerVersion").toString(); + packet.header_length = + static_cast(object.value("headerLength").toDouble()); + packet.body_length = + static_cast(object.value("bodyLength").toDouble()); + packet.length_type = object.value("lengthType").toString(); + // Absent for the packet types that carry no version octet, which is a fact + // about the type rather than a defect in the document. + packet.version = object.value("version").toInt(-1); + packet.fields = ParseFields(object.value("fields")); + packet.error = object.value("error").toString(); + + for (const auto& child : object.value("children").toArray()) { + packet.children.push_back(ParsePacket(child.toObject())); + } + return packet; +} + +auto ParseBlock(const QJsonObject& object) -> PGPInspectBlock { + PGPInspectBlock block; + block.kind = object.value("kind").toString(); + block.offset = static_cast(object.value("offset").toDouble()); + block.error = object.value("error").toString(); + + if (object.value("armor").isObject()) { + const auto armor = object.value("armor").toObject(); + block.has_armor = true; + block.armor.block_type = armor.value("blockType").toString(); + block.armor.headers = ParseFields(armor.value("headers")); + block.armor.crc24 = armor.value("crc24").toString(); + } + + if (object.value("cleartext").isObject()) { + const auto cleartext = object.value("cleartext").toObject(); + block.has_cleartext = true; + block.cleartext_text_size = + static_cast(cleartext.value("textSize").toDouble()); + block.cleartext_headers = ParseFields(cleartext.value("headers")); + } + + for (const auto& packet : object.value("packets").toArray()) { + block.packets.push_back(ParsePacket(packet.toObject())); + } + return block; +} + +auto CountPackets(const QVector& packets) -> int { + int count = 0; + for (const auto& packet : packets) { + count += 1 + CountPackets(packet.children); + } + return count; +} + +} // namespace + +auto PGPInspectDocument::PacketCount() const -> int { + int count = 0; + for (const auto& block : blocks) count += CountPackets(block.packets); + return count; +} + +auto ParsePGPInspectDocument(const QByteArray& json) -> PGPInspectDocument { + PGPInspectDocument document; + + QJsonParseError error{}; + const auto parsed = QJsonDocument::fromJson(json, &error); + if (error.error != QJsonParseError::NoError) { + document.parse_error = error.errorString(); + return document; + } + if (!parsed.isObject()) { + document.parse_error = + QCoreApplication::translate("GTrC", "The inspector returned no data."); + return document; + } + + const auto object = parsed.object(); + document.valid = true; + document.format = object.value("format").toString(); + document.size = static_cast(object.value("size").toDouble()); + + for (const auto& note : object.value("errors").toArray()) { + document.errors.append(note.toString()); + } + for (const auto& block : object.value("blocks").toArray()) { + document.blocks.push_back(ParseBlock(block.toObject())); + } + return document; +} + +auto PGPInspectHasStructure(const PGPInspectDocument& document) -> bool { + return document.valid && document.PacketCount() > 0; +} + +auto PGPInspectPacketSummary(const PGPInspectPacket& packet, int index) + -> QString { + // "#1 Signature (tag 2) v4 @0x0000 189 B". The offset is hexadecimal + // because that is what every other packet dumper prints, and what a reader + // comparing two tools needs in order to line them up. + auto summary = QCoreApplication::translate("GTrC", "#%1 %2 (tag %3)") + .arg(index) + .arg(packet.tag_name.isEmpty() + ? QCoreApplication::translate("GTrC", "Unknown") + : packet.tag_name) + .arg(packet.tag); + + if (packet.version >= 0) summary += QString(" v%1").arg(packet.version); + + summary += QString(" @0x%1").arg(packet.offset, 4, 16, QChar('0')); + summary += QCoreApplication::translate("GTrC", " %1 B") + .arg(packet.header_length + packet.body_length); + + if (packet.length_type != QLatin1String("fixed") && + !packet.length_type.isEmpty()) { + summary += QString(" [%1]").arg(packet.length_type); + } + return summary; +} + +auto PGPInspectBlockSummary(const PGPInspectBlock& block, int index) + -> QString { + if (block.kind == QLatin1String("cleartext")) { + return QCoreApplication::translate("GTrC", + "Block %1: cleartext-signed message") + .arg(index); + } + if (block.has_armor) { + return QCoreApplication::translate("GTrC", "Block %1: armored, %2") + .arg(index) + .arg(block.armor.block_type); + } + return QCoreApplication::translate("GTrC", "Block %1: binary packet stream") + .arg(index); +} diff --git a/src/m_pgp_inspect/PGPInspectModel.h b/src/m_pgp_inspect/PGPInspectModel.h new file mode 100644 index 0000000..0a081aa --- /dev/null +++ b/src/m_pgp_inspect/PGPInspectModel.h @@ -0,0 +1,141 @@ +/** + * Copyright (C) 2021-2024 Saturneric + * + * This file is part of GpgFrontend. + * + * GpgFrontend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GpgFrontend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GpgFrontend. If not, see . + * + * The initial version of the source code is inherited from + * the gpg4usb project, which is under GPL-3.0-or-later. + * + * All the source code of GpgFrontend was modified and released by + * Saturneric starting on May 12, 2021. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#pragma once + +#include +#include +#include +#include + +/** + * @file PGPInspectModel.h + * @brief The inspector's document, and the JSON that carries it. + * + * Deliberately free of every SDK and widget symbol. The parsing is the part + * with rules worth checking -- absent fields, malformed documents, the name + * tables -- and keeping it here is what lets a plain gtest binary check them + * without a GUI thread or a loaded host. The dialog stays a dumb view over + * what comes out. + */ + +/// One label / value row in a packet's detail table. +struct PGPInspectField { + QString label; + QString value; +}; + +/// A packet, and whatever is nested inside it. +struct PGPInspectPacket { + /// The numeric packet type ID; -1 when the document did not say. + int tag = -1; + QString tag_name; + qint64 offset = 0; + /// "old" or "new". + QString header_version; + qint64 header_length = 0; + qint64 body_length = 0; + /// "fixed", "partial" or "indeterminate". + QString length_type; + /// The body's own version octet; -1 for the types that carry none. + int version = -1; + QVector fields; + QVector children; + /// Why this packet could not be decoded, empty when it could. + QString error; + + [[nodiscard]] auto Malformed() const -> bool { return !error.isEmpty(); } +}; + +/// The armor envelope of a block. +struct PGPInspectArmor { + QString block_type; + QVector headers; + /// "ok", "mismatch" or "absent". + QString crc24; +}; + +/// One armor block, or the whole input when it carries none. +struct PGPInspectBlock { + /// "binary", "armored" or "cleartext". + QString kind; + qint64 offset = 0; + bool has_armor = false; + PGPInspectArmor armor; + bool has_cleartext = false; + qint64 cleartext_text_size = 0; + QVector cleartext_headers; + QVector packets; + QString error; +}; + +/// What an inspected input turned out to be. +struct PGPInspectDocument { + /// False when the JSON could not be read at all; see @ref parse_error. + bool valid = false; + QString format; + qint64 size = 0; + QVector blocks; + QStringList errors; + QString parse_error; + + /// Every packet in the document, containers before their contents. + [[nodiscard]] auto PacketCount() const -> int; +}; + +/** + * @brief Read the inspector's JSON document. + * + * Never throws and never returns a half-built document: anything it cannot + * read leaves @ref PGPInspectDocument::valid false with a reason, and a field + * that is merely missing takes its default rather than invalidating the rest. + */ +auto ParsePGPInspectDocument(const QByteArray& json) -> PGPInspectDocument; + +/** + * @brief The one-line summary a packet gets in the tree. + * + * @param packet the packet to describe + * @param index its 1-based position among its siblings + */ +auto PGPInspectPacketSummary(const PGPInspectPacket& packet, int index) + -> QString; + +/// A block's one-line summary: what kind it is, and what it wraps. +auto PGPInspectBlockSummary(const PGPInspectBlock& block, int index) -> QString; + +/** + * @brief Whether a document holds a structure worth opening a window for. + * + * A valid document with no packets in it is the ordinary answer for plain + * text, a half-written note, or anything else that simply is not OpenPGP -- + * so it is what the menu entry is greyed out on. The distinction matters: + * "this is not OpenPGP" is a finding, and offering to display it would make + * the user open a window to be told nothing. + */ +auto PGPInspectHasStructure(const PGPInspectDocument& document) -> bool; diff --git a/src/m_pgp_inspect/PGPInspectModule.cpp b/src/m_pgp_inspect/PGPInspectModule.cpp new file mode 100644 index 0000000..22dcccd --- /dev/null +++ b/src/m_pgp_inspect/PGPInspectModule.cpp @@ -0,0 +1,132 @@ +/** + * Copyright (C) 2021-2024 Saturneric + * + * This file is part of GpgFrontend. + * + * GpgFrontend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GpgFrontend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GpgFrontend. If not, see . + * + * The initial version of the source code is inherited from + * the gpg4usb project, which is under GPL-3.0-or-later. + * + * All the source code of GpgFrontend was modified and released by + * Saturneric starting on May 12, 2021. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#include + +#include +#include +#include +#include +#include + +#include "GFModuleIdentity.h" +#include "PGPInspectDialog.h" + +namespace { + +/// The ceiling on what is read out of a tab. Past this, the menu entry stays +/// greyed out rather than making every menu opening pay for a packet walk. +constexpr qint64 kMaxInspectSize = 16LL * 1024 * 1024; + +/// The current tab's document, or an invalid one when there is nothing to +/// show. Cheap enough to run when the menu opens: the walk is a linear pass +/// over at most kMaxInspectSize bytes and decrypts nothing. +auto InspectCurrentTab() -> PGPInspectDocument { + const auto bytes = PGPInspectCurrentTabBytes(); + if (bytes.isEmpty() || bytes.size() > kMaxInspectSize) return {}; + return PGPInspectBytes(bytes); +} + +void RaiseInspectDialog(QWidget *parent) { + const auto bytes = PGPInspectCurrentTabBytes(); + auto *dialog = + new PGPInspectDialog(PGPInspectBytes(bytes), bytes.size(), parent); + dialog->setModal(false); + dialog->show(); +} + +} // namespace + +auto OnActivate() -> GFResult { + // Nothing to register with the host: the module owns no settings page, no + // tab view and no metatype. Its whole surface is one menu action, added + // when the menu is mounted. + return GFResult::Ok(); +} + +auto OnMainWindowMenuMounted(const GFEvent &event) -> GFEventResult { + QMainWindow *main_window = nullptr; + if (auto r = event.RequireGui("main_window", main_window); !r.ok) return r; + + QMenu *advance_menu = nullptr; + if (auto r = event.RequireGui("advance_menu", advance_menu); !r.ok) return r; + + LOG_DEBUG("adding openpgp structure inspector to the advanced menu"); + + QMetaObject::invokeMethod( + QApplication::instance(), + [=]() -> void { + auto *action = new QAction( + QCoreApplication::translate("GTrC", "Inspect OpenPGP Structure"), + nullptr); + action->setToolTip(QCoreApplication::translate( + "GTrC", + "Show the packet structure of the current tab, without " + "decrypting it")); + QObject::connect(action, &QAction::triggered, main_window, + [=]() { RaiseInspectDialog(main_window); }); + advance_menu->addAction(action); + + // Decided when the menu opens rather than once at mount time: what + // the current tab holds changes constantly, and an entry that is + // enabled over plain text promises a window that would have nothing + // in it. + QPointer guarded(action); + QObject::connect(advance_menu, &QMenu::aboutToShow, action, [guarded] { + if (guarded.isNull()) return; + guarded->setEnabled(PGPInspectHasStructure(InspectCurrentTab())); + }); + }, + Qt::BlockingQueuedConnection); + + return GFEventResult::Ok(); +} + +// The module's whole framework surface: which events it handles, what runs at +// each lifecycle point, and one forwarder to the runtime that implements all +// of it. Everything above is business logic. +constexpr GFEventBinding kEvents[] = { + {"MAINWINDOW_MENU_MOUNTED", &OnMainWindowMenuMounted}, +}; + +constexpr GFModuleHooks kHooks = { + sizeof(GFModuleHooks), + GF_MODULE_ID, + GF_MODULE_VERSION, + GF_MODULE_TRANSLATION_CONTEXT, + &OnActivate, + nullptr, // nothing registered with the host, so nothing to undo + nullptr, + kEvents, + std::size(kEvents), +}; + +extern "C" GF_MODULE_EXPORT auto GFModuleGetApi(uint32_t abi) + -> const GFModuleApi * { + return GFModuleRuntimeGetApi(abi, &kHooks); +} diff --git a/src/m_pgp_inspect/module.json b/src/m_pgp_inspect/module.json new file mode 100644 index 0000000..92094bf --- /dev/null +++ b/src/m_pgp_inspect/module.json @@ -0,0 +1,15 @@ +{ + "schema_version": 1, + "id": "com.bktus.gpgfrontend.module.pgp_inspect", + "version": "1.0.0", + "name": "PGPInspect", + "description": "Inspect the packet structure of any OpenPGP data.", + "author": "Saturneric", + "capabilities": [ + "ui" + ], + "events": [ + "MAINWINDOW_MENU_MOUNTED" + ], + "translation_context": "ModulePGPInspect" +} diff --git a/src/m_pgp_inspect/test/CMakeLists.txt b/src/m_pgp_inspect/test/CMakeLists.txt new file mode 100644 index 0000000..a213bc7 --- /dev/null +++ b/src/m_pgp_inspect/test/CMakeLists.txt @@ -0,0 +1,53 @@ +# Copyright (C) 2021-2024 Saturneric +# +# This file is part of GpgFrontend. +# +# GpgFrontend is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# GpgFrontend is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with GpgFrontend. If not, see . +# +# The initial version of the source code is inherited from +# the gpg4usb project, which is under GPL-3.0-or-later. +# +# All the source code of GpgFrontend was modified and released by +# Saturneric starting on May 12, 2021. +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# Unit tests for the SDK-free half of the OpenPGP structure inspector. +# +# PGPInspectModel.cpp carries no GF SDK symbol and no widget, so it compiles +# straight into a plain gtest binary with nothing but Qt Core behind it. That +# is the whole reason the split exists; keep it that way, or this target stops +# linking. + +if(NOT TARGET GTest::gtest) + find_package(GTest QUIET) +endif() + +if(NOT TARGET GTest::gtest) + message(STATUS "GTest not found; skipping OpenPGP inspector module tests") + return() +endif() + +add_executable(gf_mod_pgp_inspect_test + PGPInspectModelTest.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../PGPInspectModel.cpp) + +target_include_directories(gf_mod_pgp_inspect_test PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/..") + +target_link_libraries(gf_mod_pgp_inspect_test PRIVATE + GTest::gtest GTest::gtest_main Qt::Core) + +set_target_properties(gf_mod_pgp_inspect_test PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") diff --git a/src/m_pgp_inspect/test/PGPInspectModelTest.cpp b/src/m_pgp_inspect/test/PGPInspectModelTest.cpp new file mode 100644 index 0000000..3815176 --- /dev/null +++ b/src/m_pgp_inspect/test/PGPInspectModelTest.cpp @@ -0,0 +1,298 @@ +/** + * Copyright (C) 2021-2024 Saturneric + * + * This file is part of GpgFrontend. + * + * GpgFrontend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GpgFrontend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GpgFrontend. If not, see . + * + * The initial version of the source code is inherited from + * the gpg4usb project, which is under GPL-3.0-or-later. + * + * All the source code of GpgFrontend was modified and released by + * Saturneric starting on May 12, 2021. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#include + +#include + +#include "PGPInspectModel.h" + +namespace { + +/// A minimal but complete document, in the shape the Rust walk emits. +constexpr const char* kDocument = R"({ + "format": "armored", + "size": 325, + "errors": [], + "blocks": [ + { + "kind": "armored", + "offset": 0, + "armor": { + "blockType": "PGP SIGNATURE", + "headers": [{"label": "Version", "value": "GnuPG v2"}], + "crc24": "ok" + }, + "packets": [ + { + "tag": 8, + "tagName": "Compressed Data", + "offset": 0, + "headerVersion": "new", + "headerLength": 2, + "bodyLength": 189, + "lengthType": "fixed", + "fields": [{"label": "Compression Algorithm", "value": "ZIP"}], + "children": [ + { + "tag": 2, + "tagName": "Signature", + "offset": 0, + "headerVersion": "old", + "headerLength": 3, + "bodyLength": 100, + "lengthType": "fixed", + "version": 4, + "fields": [{"label": "Hash Algorithm", "value": "Sha256"}], + "children": [], + "error": null + } + ], + "error": null + } + ], + "error": null + } + ] +})"; + +} // namespace + +// -- reading the document ---------------------------------------------------- + +TEST(PGPInspectModelTest, ReadsTheDocumentShape) { + const auto document = ParsePGPInspectDocument(kDocument); + + ASSERT_TRUE(document.valid); + EXPECT_EQ(document.format, QString("armored")); + EXPECT_EQ(document.size, 325); + ASSERT_EQ(document.blocks.size(), 1); + EXPECT_TRUE(document.errors.isEmpty()); +} + +TEST(PGPInspectModelTest, ReadsTheArmorEnvelope) { + const auto document = ParsePGPInspectDocument(kDocument); + const auto& block = document.blocks.at(0); + + ASSERT_TRUE(block.has_armor); + EXPECT_EQ(block.armor.block_type, QString("PGP SIGNATURE")); + EXPECT_EQ(block.armor.crc24, QString("ok")); + ASSERT_EQ(block.armor.headers.size(), 1); + EXPECT_EQ(block.armor.headers.at(0).label, QString("Version")); +} + +TEST(PGPInspectModelTest, NestsChildrenUnderTheirContainer) { + // The recursion is the reason the tree exists; a flat read would lose it. + const auto document = ParsePGPInspectDocument(kDocument); + const auto& packets = document.blocks.at(0).packets; + + ASSERT_EQ(packets.size(), 1); + EXPECT_EQ(packets.at(0).tag, 8); + ASSERT_EQ(packets.at(0).children.size(), 1); + EXPECT_EQ(packets.at(0).children.at(0).tag, 2); + EXPECT_EQ(packets.at(0).children.at(0).version, 4); +} + +TEST(PGPInspectModelTest, CountsNestedPacketsToo) { + EXPECT_EQ(ParsePGPInspectDocument(kDocument).PacketCount(), 2); +} + +TEST(PGPInspectModelTest, KeepsTheFramingFiguresVerbatim) { + const auto& packet = + ParsePGPInspectDocument(kDocument).blocks.at(0).packets.at(0); + + EXPECT_EQ(packet.offset, 0); + EXPECT_EQ(packet.header_length, 2); + EXPECT_EQ(packet.body_length, 189); + EXPECT_EQ(packet.header_version, QString("new")); + EXPECT_EQ(packet.length_type, QString("fixed")); +} + +TEST(PGPInspectModelTest, AbsentVersionReadsAsMinusOne) { + // Most container packets carry no version octet. That is a fact about the + // type, so it must not look like a defect in the document. + const auto& packet = + ParsePGPInspectDocument(kDocument).blocks.at(0).packets.at(0); + EXPECT_EQ(packet.version, -1); + EXPECT_FALSE(packet.Malformed()); +} + +// -- the shapes that are not the happy one ------------------------------------ + +TEST(PGPInspectModelTest, MalformedJsonIsReportedNotThrown) { + const auto document = ParsePGPInspectDocument("{ this is not json"); + + EXPECT_FALSE(document.valid); + EXPECT_FALSE(document.parse_error.isEmpty()); + EXPECT_TRUE(document.blocks.isEmpty()); +} + +TEST(PGPInspectModelTest, EmptyInputIsReportedNotThrown) { + const auto document = ParsePGPInspectDocument({}); + + EXPECT_FALSE(document.valid); + EXPECT_FALSE(document.parse_error.isEmpty()); +} + +TEST(PGPInspectModelTest, ANonObjectDocumentIsRefused) { + EXPECT_FALSE(ParsePGPInspectDocument("[1, 2, 3]").valid); +} + +TEST(PGPInspectModelTest, AnEmptyDocumentIsValidAndEmpty) { + // "nothing recognisable here" is a finding the inspector reports, not a + // failure to read its answer. + const auto document = ParsePGPInspectDocument( + R"({"format":"binary","size":4,"blocks":[],"errors":["not OpenPGP data"]})"); + + ASSERT_TRUE(document.valid); + EXPECT_TRUE(document.blocks.isEmpty()); + ASSERT_EQ(document.errors.size(), 1); + EXPECT_EQ(document.errors.at(0), QString("not OpenPGP data")); +} + +TEST(PGPInspectModelTest, APacketErrorSurvivesIntoTheTree) { + const auto document = ParsePGPInspectDocument( + R"({"blocks":[{"packets":[{"tag":2,"error":"the packet body is truncated"}]}]})"); + + ASSERT_EQ(document.blocks.size(), 1); + const auto& packet = document.blocks.at(0).packets.at(0); + EXPECT_TRUE(packet.Malformed()); + EXPECT_EQ(packet.error, QString("the packet body is truncated")); +} + +TEST(PGPInspectModelTest, MissingFieldsTakeTheirDefaults) { + // A document from an older or newer engine must degrade field by field + // rather than costing the reader the whole tree. + const auto document = + ParsePGPInspectDocument(R"({"blocks":[{"packets":[{}]}]})"); + + ASSERT_TRUE(document.valid); + const auto& packet = document.blocks.at(0).packets.at(0); + EXPECT_EQ(packet.tag, -1); + EXPECT_TRUE(packet.tag_name.isEmpty()); + EXPECT_EQ(packet.body_length, 0); +} + +TEST(PGPInspectModelTest, ADetailRowWithNeitherHalfIsDropped) { + const auto document = ParsePGPInspectDocument( + R"({"blocks":[{"packets":[{"fields":[{},{"label":"A","value":"B"}]}]}]})"); + + const auto& fields = document.blocks.at(0).packets.at(0).fields; + ASSERT_EQ(fields.size(), 1); + EXPECT_EQ(fields.at(0).label, QString("A")); +} + +TEST(PGPInspectModelTest, ReadsTheCleartextHalf) { + const auto document = ParsePGPInspectDocument( + R"({"format":"cleartext","blocks":[{"kind":"cleartext","cleartext":{"textSize":42,"headers":[{"label":"Hash","value":"SHA256"}]},"packets":[]}]})"); + + const auto& block = document.blocks.at(0); + ASSERT_TRUE(block.has_cleartext); + EXPECT_EQ(block.cleartext_text_size, 42); + ASSERT_EQ(block.cleartext_headers.size(), 1); + EXPECT_EQ(block.cleartext_headers.at(0).value, QString("SHA256")); +} + +// -- whether there is anything to show ---------------------------------------- + +TEST(PGPInspectModelTest, ADocumentWithPacketsHasStructure) { + EXPECT_TRUE(PGPInspectHasStructure(ParsePGPInspectDocument(kDocument))); +} + +TEST(PGPInspectModelTest, PlainTextHasNoStructure) { + // The menu entry is greyed out on exactly this: a valid document saying the + // input is not OpenPGP. Opening a window to be told nothing is worse than + // an entry that cannot be clicked. + EXPECT_FALSE(PGPInspectHasStructure(ParsePGPInspectDocument( + R"({"format":"binary","size":9,"blocks":[],"errors":["not OpenPGP data"]})"))); +} + +TEST(PGPInspectModelTest, AnUnreadableDocumentHasNoStructure) { + EXPECT_FALSE(PGPInspectHasStructure(ParsePGPInspectDocument("not json"))); + EXPECT_FALSE(PGPInspectHasStructure({})); +} + +TEST(PGPInspectModelTest, ABlockWithNoPacketsHasNoStructure) { + // An armor envelope whose body decoded to nothing: the header parsed, so + // the document is valid, but there is still no packet tree to render. + EXPECT_FALSE(PGPInspectHasStructure(ParsePGPInspectDocument( + R"({"format":"armored","blocks":[{"kind":"armored","packets":[]}]})"))); +} + +TEST(PGPInspectModelTest, ANestedPacketAloneIsStructureEnough) { + EXPECT_TRUE(PGPInspectHasStructure(ParsePGPInspectDocument( + R"({"blocks":[{"packets":[{"tag":8,"children":[{"tag":2}]}]}]})"))); +} + +// -- the summaries the tree shows --------------------------------------------- + +TEST(PGPInspectModelTest, APacketSummaryNamesTagVersionOffsetAndSize) { + const auto& packet = + ParsePGPInspectDocument(kDocument).blocks.at(0).packets.at(0).children.at( + 0); + const auto summary = PGPInspectPacketSummary(packet, 1); + + EXPECT_TRUE(summary.contains("#1")); + EXPECT_TRUE(summary.contains("Signature")); + EXPECT_TRUE(summary.contains("tag 2")); + EXPECT_TRUE(summary.contains("v4")); + EXPECT_TRUE(summary.contains("@0x0000")); + // Header plus body: the bytes this packet occupies on the wire. + EXPECT_TRUE(summary.contains("103")); +} + +TEST(PGPInspectModelTest, ASummaryFlagsANonFixedLength) { + PGPInspectPacket packet; + packet.tag = 11; + packet.tag_name = "Literal Data"; + packet.length_type = "partial"; + + EXPECT_TRUE(PGPInspectPacketSummary(packet, 2).contains("partial")); +} + +TEST(PGPInspectModelTest, ASummaryOfANamelessPacketStillReads) { + PGPInspectPacket packet; + packet.tag = 60; + + const auto summary = PGPInspectPacketSummary(packet, 1); + EXPECT_TRUE(summary.contains("Unknown")); + EXPECT_TRUE(summary.contains("tag 60")); +} + +TEST(PGPInspectModelTest, ABlockSummaryDistinguishesTheThreeKinds) { + const auto armored = ParsePGPInspectDocument(kDocument).blocks.at(0); + EXPECT_TRUE(PGPInspectBlockSummary(armored, 1).contains("PGP SIGNATURE")); + + PGPInspectBlock cleartext; + cleartext.kind = "cleartext"; + EXPECT_TRUE(PGPInspectBlockSummary(cleartext, 1).contains("cleartext")); + + PGPInspectBlock binary; + binary.kind = "binary"; + EXPECT_TRUE(PGPInspectBlockSummary(binary, 1).contains("binary")); +} diff --git a/src/m_pgp_inspect/ts/ModulePGPInspect.de_DE.ts b/src/m_pgp_inspect/ts/ModulePGPInspect.de_DE.ts new file mode 100644 index 0000000..7b67b95 --- /dev/null +++ b/src/m_pgp_inspect/ts/ModulePGPInspect.de_DE.ts @@ -0,0 +1,105 @@ + + + + + GTrC + + + OpenPGP Structure + + + + + Structure + + + + + Value + + + + + Search packets, fields and values + + + + + The current tab holds no OpenPGP data. + + + + + Armor Type + + + + + CRC24 + + + + + Signed Text + + + + + %1, %2, %n packet(s) + + + + + + + + Problem + + + + + The inspector returned no data. + + + + + #%1 %2 (tag %3) + + + + + Unknown + + + + + %1 B + + + + + Block %1: cleartext-signed message + + + + + Block %1: armored, %2 + + + + + Block %1: binary packet stream + + + + + Inspect OpenPGP Structure + + + + + Show the packet structure of the current tab, without decrypting it + + + + diff --git a/src/m_pgp_inspect/ts/ModulePGPInspect.en_US.ts b/src/m_pgp_inspect/ts/ModulePGPInspect.en_US.ts new file mode 100644 index 0000000..7b67b95 --- /dev/null +++ b/src/m_pgp_inspect/ts/ModulePGPInspect.en_US.ts @@ -0,0 +1,105 @@ + + + + + GTrC + + + OpenPGP Structure + + + + + Structure + + + + + Value + + + + + Search packets, fields and values + + + + + The current tab holds no OpenPGP data. + + + + + Armor Type + + + + + CRC24 + + + + + Signed Text + + + + + %1, %2, %n packet(s) + + + + + + + + Problem + + + + + The inspector returned no data. + + + + + #%1 %2 (tag %3) + + + + + Unknown + + + + + %1 B + + + + + Block %1: cleartext-signed message + + + + + Block %1: armored, %2 + + + + + Block %1: binary packet stream + + + + + Inspect OpenPGP Structure + + + + + Show the packet structure of the current tab, without decrypting it + + + + diff --git a/src/m_pgp_inspect/ts/ModulePGPInspect.es_ES.ts b/src/m_pgp_inspect/ts/ModulePGPInspect.es_ES.ts new file mode 100644 index 0000000..7b67b95 --- /dev/null +++ b/src/m_pgp_inspect/ts/ModulePGPInspect.es_ES.ts @@ -0,0 +1,105 @@ + + + + + GTrC + + + OpenPGP Structure + + + + + Structure + + + + + Value + + + + + Search packets, fields and values + + + + + The current tab holds no OpenPGP data. + + + + + Armor Type + + + + + CRC24 + + + + + Signed Text + + + + + %1, %2, %n packet(s) + + + + + + + + Problem + + + + + The inspector returned no data. + + + + + #%1 %2 (tag %3) + + + + + Unknown + + + + + %1 B + + + + + Block %1: cleartext-signed message + + + + + Block %1: armored, %2 + + + + + Block %1: binary packet stream + + + + + Inspect OpenPGP Structure + + + + + Show the packet structure of the current tab, without decrypting it + + + + diff --git a/src/m_pgp_inspect/ts/ModulePGPInspect.fr_FR.ts b/src/m_pgp_inspect/ts/ModulePGPInspect.fr_FR.ts new file mode 100644 index 0000000..7b67b95 --- /dev/null +++ b/src/m_pgp_inspect/ts/ModulePGPInspect.fr_FR.ts @@ -0,0 +1,105 @@ + + + + + GTrC + + + OpenPGP Structure + + + + + Structure + + + + + Value + + + + + Search packets, fields and values + + + + + The current tab holds no OpenPGP data. + + + + + Armor Type + + + + + CRC24 + + + + + Signed Text + + + + + %1, %2, %n packet(s) + + + + + + + + Problem + + + + + The inspector returned no data. + + + + + #%1 %2 (tag %3) + + + + + Unknown + + + + + %1 B + + + + + Block %1: cleartext-signed message + + + + + Block %1: armored, %2 + + + + + Block %1: binary packet stream + + + + + Inspect OpenPGP Structure + + + + + Show the packet structure of the current tab, without decrypting it + + + + diff --git a/src/m_pgp_inspect/ts/ModulePGPInspect.it_IT.ts b/src/m_pgp_inspect/ts/ModulePGPInspect.it_IT.ts new file mode 100644 index 0000000..7b67b95 --- /dev/null +++ b/src/m_pgp_inspect/ts/ModulePGPInspect.it_IT.ts @@ -0,0 +1,105 @@ + + + + + GTrC + + + OpenPGP Structure + + + + + Structure + + + + + Value + + + + + Search packets, fields and values + + + + + The current tab holds no OpenPGP data. + + + + + Armor Type + + + + + CRC24 + + + + + Signed Text + + + + + %1, %2, %n packet(s) + + + + + + + + Problem + + + + + The inspector returned no data. + + + + + #%1 %2 (tag %3) + + + + + Unknown + + + + + %1 B + + + + + Block %1: cleartext-signed message + + + + + Block %1: armored, %2 + + + + + Block %1: binary packet stream + + + + + Inspect OpenPGP Structure + + + + + Show the packet structure of the current tab, without decrypting it + + + + diff --git a/src/m_pgp_inspect/ts/ModulePGPInspect.pl_PL.ts b/src/m_pgp_inspect/ts/ModulePGPInspect.pl_PL.ts new file mode 100644 index 0000000..7b67b95 --- /dev/null +++ b/src/m_pgp_inspect/ts/ModulePGPInspect.pl_PL.ts @@ -0,0 +1,105 @@ + + + + + GTrC + + + OpenPGP Structure + + + + + Structure + + + + + Value + + + + + Search packets, fields and values + + + + + The current tab holds no OpenPGP data. + + + + + Armor Type + + + + + CRC24 + + + + + Signed Text + + + + + %1, %2, %n packet(s) + + + + + + + + Problem + + + + + The inspector returned no data. + + + + + #%1 %2 (tag %3) + + + + + Unknown + + + + + %1 B + + + + + Block %1: cleartext-signed message + + + + + Block %1: armored, %2 + + + + + Block %1: binary packet stream + + + + + Inspect OpenPGP Structure + + + + + Show the packet structure of the current tab, without decrypting it + + + + diff --git a/src/m_pgp_inspect/ts/ModulePGPInspect.ru_RU.ts b/src/m_pgp_inspect/ts/ModulePGPInspect.ru_RU.ts new file mode 100644 index 0000000..7b67b95 --- /dev/null +++ b/src/m_pgp_inspect/ts/ModulePGPInspect.ru_RU.ts @@ -0,0 +1,105 @@ + + + + + GTrC + + + OpenPGP Structure + + + + + Structure + + + + + Value + + + + + Search packets, fields and values + + + + + The current tab holds no OpenPGP data. + + + + + Armor Type + + + + + CRC24 + + + + + Signed Text + + + + + %1, %2, %n packet(s) + + + + + + + + Problem + + + + + The inspector returned no data. + + + + + #%1 %2 (tag %3) + + + + + Unknown + + + + + %1 B + + + + + Block %1: cleartext-signed message + + + + + Block %1: armored, %2 + + + + + Block %1: binary packet stream + + + + + Inspect OpenPGP Structure + + + + + Show the packet structure of the current tab, without decrypting it + + + + diff --git a/src/m_pgp_inspect/ts/ModulePGPInspect.zh_CN.ts b/src/m_pgp_inspect/ts/ModulePGPInspect.zh_CN.ts new file mode 100644 index 0000000..7b67b95 --- /dev/null +++ b/src/m_pgp_inspect/ts/ModulePGPInspect.zh_CN.ts @@ -0,0 +1,105 @@ + + + + + GTrC + + + OpenPGP Structure + + + + + Structure + + + + + Value + + + + + Search packets, fields and values + + + + + The current tab holds no OpenPGP data. + + + + + Armor Type + + + + + CRC24 + + + + + Signed Text + + + + + %1, %2, %n packet(s) + + + + + + + + Problem + + + + + The inspector returned no data. + + + + + #%1 %2 (tag %3) + + + + + Unknown + + + + + %1 B + + + + + Block %1: cleartext-signed message + + + + + Block %1: armored, %2 + + + + + Block %1: binary packet stream + + + + + Inspect OpenPGP Structure + + + + + Show the packet structure of the current tab, without decrypting it + + + + diff --git a/src/m_pgp_inspect/ts/ModulePGPInspect.zh_TW.ts b/src/m_pgp_inspect/ts/ModulePGPInspect.zh_TW.ts new file mode 100644 index 0000000..7b67b95 --- /dev/null +++ b/src/m_pgp_inspect/ts/ModulePGPInspect.zh_TW.ts @@ -0,0 +1,105 @@ + + + + + GTrC + + + OpenPGP Structure + + + + + Structure + + + + + Value + + + + + Search packets, fields and values + + + + + The current tab holds no OpenPGP data. + + + + + Armor Type + + + + + CRC24 + + + + + Signed Text + + + + + %1, %2, %n packet(s) + + + + + + + + Problem + + + + + The inspector returned no data. + + + + + #%1 %2 (tag %3) + + + + + Unknown + + + + + %1 B + + + + + Block %1: cleartext-signed message + + + + + Block %1: armored, %2 + + + + + Block %1: binary packet stream + + + + + Inspect OpenPGP Structure + + + + + Show the packet structure of the current tab, without decrypting it + + + + -- cgit