KUnifiedPush

contentencryption.cpp
1/*
2 SPDX-FileCopyrightText: 2025 Volker Krause <vkrause@kde.org>
3 SPDX-License-Identifier: LGPL-2.0-or-later
4*/
5
6#include "contentencryption_p.h"
7#include "logging.h"
8
9#include "../shared/contentencryptionutils_p.h"
10#include "../shared/eckey_p.h"
11#include "../shared/opensslpp_p.h"
12
13#include <QtEndian>
14
15#include <openssl/ec.h>
16#include <openssl/err.h>
17
18using namespace KUnifiedPush;
19
20namespace KUnifiedPush {
21class ContentEncryptionPrivate {
22public:
23 QByteArray m_publicKey;
24 QByteArray m_privateKey;
25 QByteArray m_authSecret;
26};
27}
28
29ContentEncryption::ContentEncryption() = default;
30ContentEncryption::ContentEncryption(ContentEncryption &&) noexcept = default;
31
32ContentEncryption::ContentEncryption(const QByteArray &publicKey, const QByteArray &privateKey, const QByteArray &authSecret)
33 : d(std::make_unique<ContentEncryptionPrivate>())
34{
35 d->m_publicKey = publicKey;
36 d->m_privateKey = privateKey;
37 d->m_authSecret = authSecret;
38}
39
40ContentEncryption::~ContentEncryption() = default;
41
42ContentEncryption& ContentEncryption::operator=(ContentEncryption&&) noexcept = default;
43
44QByteArray ContentEncryption::publicKey() const
45{
46 return d ? d->m_publicKey : QByteArray();
47}
48
49QByteArray ContentEncryption::privateKey() const
50{
51 return d ? d->m_privateKey : QByteArray();
52}
53
54QByteArray ContentEncryption::authSecret() const
55{
56 return d ? d->m_authSecret : QByteArray();
57}
58
59bool ContentEncryption::hasKeys() const
60{
61 // private key should be 32 byte long, but leading zeros are not included
62 return d && d->m_publicKey.size() == 65 && !d->m_privateKey.isEmpty() && d->m_privateKey.size() <= 32 && d->m_authSecret.size() == CE_AUTH_SECRET_SIZE;
63}
64
65QByteArray ContentEncryption::decrypt(const QByteArray &encrypted) const
66{
67 if (!hasKeys()) {
68 return {};
69 }
70
71 if (encrypted.size() < 22) {
72 qCWarning(Log) << "Encrypted message is too short!";
73 return {};
74 }
75
76 // decode header according to RFC 8188 ยง2.1
77 const auto salt = QByteArrayView(encrypted).left(CE_SALT_SIZE);
78 if (const auto rs = qFromBigEndian(*reinterpret_cast<const uint32_t*>(encrypted.constData() + 16)); rs != CE_RECORD_SIZE) {
79 qCWarning(Log) << "unexpected rs:" << rs;
80 return {};
81 }
82 const auto idlen = *reinterpret_cast<const uint8_t*>(encrypted.constData() + 20);
83 if (encrypted.size() < 22 + idlen + CE_AEAD_TAG_SIZE) {
84 qCWarning(Log) << "idlen exceeds encrypted message size!";
85 return {};
86 }
87 const auto keyid = QByteArrayView(encrypted).mid(21, idlen); // sender public key in RFC 8291
88 const auto encryptedContent = QByteArrayView(encrypted).mid(21 + idlen);
89
90 // load user agent key pair
91 const auto pkey = ECKey::load(d->m_publicKey, d->m_privateKey);
92 const auto peerKey = ECKey::load(keyid);
93 if (!pkey || !peerKey) {
94 qCWarning(Log) << "Failed to load EC keys!";
95 return {};
96 }
97
98 // derive ECDH shared secret
99 const auto ecdh_secret = ContentEcryptionUtils::ecdhSharedSecret(pkey, peerKey);
100 if (ecdh_secret.isEmpty()) {
101 return {};
102 }
103
104 // determine content encoding key and nonce
105 const auto prk_key = ContentEcryptionUtils::hmacSha256(d->m_authSecret, ecdh_secret);
106 const QByteArray key_info = QByteArrayView("WebPush: info") + '\x00' + d->m_publicKey + keyid + '\x01';
107 const auto ikm = ContentEcryptionUtils::hmacSha256(prk_key, key_info);
108 const auto prk = ContentEcryptionUtils::hmacSha256(salt, ikm);
109 const auto cek = ContentEcryptionUtils::cek(prk);
110 const auto nonce = ContentEcryptionUtils::nonce(prk);
111
112 // AES 128 GCM decryption with 16 byte AEAD tag
113 QByteArray plaintext(encryptedContent.size() - CE_AEAD_TAG_SIZE, Qt::Uninitialized);
114 int plaintextLen = 0, len = 0;
115
116 openssl::evp_cipher_ctx_ptr aesCtx(EVP_CIPHER_CTX_new());
117 EVP_DecryptInit(aesCtx.get(), EVP_aes_128_gcm(), reinterpret_cast<const uint8_t*>(cek.constData()), reinterpret_cast<const uint8_t*>(nonce.constData()));
118 EVP_DecryptUpdate(aesCtx.get(), reinterpret_cast<uint8_t*>(plaintext.data()), &plaintextLen, reinterpret_cast<const uint8_t*>(encryptedContent.constData()), (int)encryptedContent.size() - CE_AEAD_TAG_SIZE);
119 EVP_CIPHER_CTX_ctrl(aesCtx.get(), EVP_CTRL_GCM_SET_TAG, CE_AEAD_TAG_SIZE, const_cast<void*>(reinterpret_cast<const void*>(encryptedContent.right(CE_AEAD_TAG_SIZE).constData())));
120 if (const auto res = EVP_DecryptFinal_ex(aesCtx.get(), reinterpret_cast<uint8_t*>(plaintext.data() + plaintextLen), &len); res <= 0) {
121 qCWarning(Log) << ERR_error_string(ERR_get_error(), nullptr);
122 return {};
123 }
124 plaintextLen += len;
125
126 // remove padding
127 for (len = plaintextLen - 1; len; --len) {
128 if (plaintext[len] == CE_MESSAGE_PADDING) {
129 break;
130 }
131 }
132 plaintext.resize(len);
133
134 return plaintext;
135}
136
137ContentEncryption ContentEncryption::generateKeys()
138{
139 ContentEncryption c;
140 c.d = std::make_unique<ContentEncryptionPrivate>();
141
142 // EC keypair
143 openssl::evp_pkey_ptr key(EVP_EC_gen("prime256v1"));
144 if (!key) {
145 return {};
146 }
147 const auto keyPair = ECKey::store(key);
148 c.d->m_publicKey = keyPair.publicKey;
149 c.d->m_privateKey = keyPair.privateKey;
150
151 // auth secret
152 c.d->m_authSecret = ContentEcryptionUtils::random(CE_AUTH_SECRET_SIZE);
153
154 return c;
155}
Client-side integration with UnifiedPush.
Definition connector.h:14
const char * constData() const const
qsizetype size() const const
QByteArrayView left(qsizetype length) const const
QByteArrayView mid(qsizetype start, qsizetype length) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 25 2025 12:05:39 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.