MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 91.0% 202 / 0 / 222
Functions: 100.0% 21 / 0 / 21
Branches: 76.1% 248 / 0 / 326

service/WebPushPayloadCodec.cpp
Line Branch Exec Source
1 #include "WebPushPayloadCodec.h"
2
3 #include "OpenSslRuntime.h"
4 #include "util/SecureUtil.h"
5
6 #include <QMessageAuthenticationCode>
7 #include <QRandomGenerator>
8 #include <QRegularExpression>
9 #include <memory>
10
11 namespace WebPushPayloadCodec {
12 namespace {
13
14 using namespace OpenSslRuntime;
15
16 // DER prefix of a P-256 SubjectPublicKeyInfo with an uncompressed point
17 // (id-ecPublicKey, prime256v1, BIT STRING of 66 bytes).
18 const QByteArray kSpkiPrefix = QByteArray::fromHex("3059301306072a8648ce3d020106082a8648ce3d030107034200");
19 constexpr int kSaltBytes = 16;
20 constexpr int kHeaderBytes = kSaltBytes + 4 + 1;
21 constexpr int kTagBytes = 16;
22
23 13 void setError(QString *error, const QString &text) {
24
1/2
✓ Branch 2 → 3 taken 13 times.
✗ Branch 2 → 4 not taken.
13 if (error) *error = text;
25 13 }
26
27 struct PkeyDeleter {
28 const Api *a;
29
1/2
✓ Branch 2 → 3 taken 105 times.
✗ Branch 2 → 4 not taken.
105 void operator()(EvpPkey *key) const { if (key) a->EVP_PKEY_free(key); }
30 };
31 struct CtxDeleter {
32 const Api *a;
33
1/2
✓ Branch 2 → 3 taken 73 times.
✗ Branch 2 → 4 not taken.
73 void operator()(EvpPkeyCtx *ctx) const { if (ctx) a->EVP_PKEY_CTX_free(ctx); }
34 };
35 struct CipherDeleter {
36 const Api *a;
37
1/2
✓ Branch 2 → 3 taken 25 times.
✗ Branch 2 → 4 not taken.
25 void operator()(EvpCipherCtx *ctx) const { if (ctx) a->EVP_CIPHER_CTX_free(ctx); }
38 };
39 using Pkey = std::unique_ptr<EvpPkey, PkeyDeleter>;
40
41 26 Pkey publicKeyFromPoint(const Api *a, const QByteArray &point) {
42
3/6
✓ Branch 3 → 4 taken 26 times.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 26 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 11 taken 26 times.
26 if (point.size() != kPublicKeyBytes || point.at(0) != 0x04) return Pkey(nullptr, {a});
43
1/1
✓ Branch 11 → 12 taken 26 times.
26 const QByteArray der = kSpkiPrefix + point;
44 26 const auto *cursor = reinterpret_cast<const unsigned char *>(der.constData());
45 // d2i_PUBKEY decodes the point and rejects points that are not on the curve.
46
1/1
✓ Branch 14 → 15 taken 26 times.
26 return Pkey(a->d2i_PUBKEY(nullptr, &cursor, der.size()), {a});
47 26 }
48
49 32 Pkey privateKeyFromDer(const Api *a, const QByteArray &der) {
50 32 const auto *cursor = reinterpret_cast<const unsigned char *>(der.constData());
51
1/1
✓ Branch 4 → 5 taken 32 times.
32 return Pkey(a->d2i_AutoPrivateKey(nullptr, &cursor, der.size()), {a});
52 }
53
54 64 QByteArray publicPoint(const Api *a, const EvpPkey *key) {
55
1/1
✓ Branch 2 → 3 taken 64 times.
64 const int length = a->i2d_PUBKEY(key, nullptr);
56
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 64 times.
64 if (length != kSpkiPrefix.size() + kPublicKeyBytes) return {};
57
1/1
✓ Branch 6 → 7 taken 64 times.
64 QByteArray der(length, Qt::Uninitialized);
58
1/1
✓ Branch 7 → 8 taken 64 times.
64 auto *cursor = reinterpret_cast<unsigned char *>(der.data());
59
4/7
✓ Branch 8 → 9 taken 64 times.
✓ Branch 9 → 10 taken 64 times.
✗ Branch 9 → 13 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 64 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 64 times.
64 if (a->i2d_PUBKEY(key, &cursor) != length || !der.startsWith(kSpkiPrefix)) return {};
60
1/1
✓ Branch 18 → 19 taken 64 times.
64 return der.mid(kSpkiPrefix.size());
61 64 }
62
63 54 QByteArray privateDer(const Api *a, const EvpPkey *key) {
64
1/1
✓ Branch 2 → 3 taken 54 times.
54 const int length = a->i2d_PrivateKey(key, nullptr);
65
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 54 times.
54 if (length <= 0) return {};
66
1/1
✓ Branch 5 → 6 taken 54 times.
54 QByteArray der(length, Qt::Uninitialized);
67
1/1
✓ Branch 6 → 7 taken 54 times.
54 auto *cursor = reinterpret_cast<unsigned char *>(der.data());
68
2/3
✓ Branch 7 → 8 taken 54 times.
✓ Branch 8 → 9 taken 54 times.
✗ Branch 8 → 10 not taken.
54 return a->i2d_PrivateKey(key, &cursor) == length ? der : QByteArray();
69 54 }
70
71 25 QByteArray ecdh(const Api *a, EvpPkey *own, EvpPkey *peer) {
72
1/1
✓ Branch 2 → 3 taken 25 times.
25 std::unique_ptr<EvpPkeyCtx, CtxDeleter> ctx(a->EVP_PKEY_CTX_new(own, nullptr), {a});
73 25 size_t length = 0;
74
4/6
✓ Branch 7 → 8 taken 25 times.
✓ Branch 8 → 9 taken 25 times.
✗ Branch 8 → 16 not taken.
✓ Branch 10 → 11 taken 25 times.
✓ Branch 11 → 12 taken 25 times.
✗ Branch 11 → 16 not taken.
50 if (!ctx || a->EVP_PKEY_derive_init(ctx.get()) <= 0 || a->EVP_PKEY_derive_set_peer(ctx.get(), peer) <= 0 ||
75
5/9
✓ Branch 5 → 6 taken 25 times.
✗ Branch 5 → 16 not taken.
✓ Branch 13 → 14 taken 25 times.
✓ Branch 14 → 15 taken 25 times.
✗ Branch 14 → 16 not taken.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 25 times.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 25 times.
50 a->EVP_PKEY_derive(ctx.get(), nullptr, &length) <= 0 || length != 32)
76 ✗ return {};
77
1/1
✓ Branch 20 → 21 taken 25 times.
25 QByteArray secret(int(length), Qt::Uninitialized);
78
3/4
✓ Branch 21 → 22 taken 25 times.
✓ Branch 23 → 24 taken 25 times.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 25 times.
25 if (a->EVP_PKEY_derive(ctx.get(), reinterpret_cast<unsigned char *>(secret.data()), &length) <= 0) return {};
79 25 return secret;
80 25 }
81
82 125 QByteArray hmacSha256(const QByteArray &key, const QByteArray &data) {
83
1/1
✓ Branch 4 → 5 taken 125 times.
125 return QMessageAuthenticationCode::hash(data, key, QCryptographicHash::Sha256);
84 }
85
86 // HKDF-Expand for outputs of at most one hash block (RFC 5869).
87 75 QByteArray hkdfExpand(const QByteArray &prk, const QByteArray &info, int length) {
88
4/4
✓ Branch 2 → 3 taken 75 times.
✓ Branch 3 → 4 taken 75 times.
✓ Branch 4 → 5 taken 75 times.
✓ Branch 5 → 6 taken 75 times.
75 return hmacSha256(prk, info + QByteArray(1, '\x01')).left(length);
89 }
90
91 struct ContentKeys {
92 QByteArray cek;
93 QByteArray nonce;
94 };
95
96 // RFC 8291 §3.3/§3.4 key derivation on top of RFC 8188 §2.2/§2.3.
97 // S106-084: every intermediate key is wiped before its buffer is freed.
98 25 ContentKeys deriveKeys(const QByteArray &ecdhSecret, const QByteArray &authSecret, const QByteArray &receiverPublic,
99 const QByteArray &senderPublic, const QByteArray &salt) {
100
1/1
✓ Branch 2 → 3 taken 25 times.
25 QByteArray prkKey = hmacSha256(authSecret, ecdhSecret);
101
4/4
✓ Branch 3 → 4 taken 25 times.
✓ Branch 4 → 5 taken 25 times.
✓ Branch 5 → 6 taken 25 times.
✓ Branch 6 → 7 taken 25 times.
25 const QByteArray keyInfo = QByteArray("WebPush: info") + '\0' + receiverPublic + senderPublic;
102
1/1
✓ Branch 10 → 11 taken 25 times.
25 QByteArray ikm = hkdfExpand(prkKey, keyInfo, 32);
103
1/1
✓ Branch 11 → 12 taken 25 times.
25 QByteArray prk = hmacSha256(salt, ikm);
104
2/2
✓ Branch 12 → 13 taken 25 times.
✓ Branch 13 → 14 taken 25 times.
50 ContentKeys keys{hkdfExpand(prk, QByteArray("Content-Encoding: aes128gcm") + '\0', 16),
105
4/6
✓ Branch 14 → 15 taken 25 times.
✓ Branch 15 → 16 taken 25 times.
✓ Branch 16 → 17 taken 25 times.
✓ Branch 17 → 18 taken 25 times.
✗ Branch 46 → 47 not taken.
✗ Branch 46 → 48 not taken.
25 hkdfExpand(prk, QByteArray("Content-Encoding: nonce") + '\0', 12)};
106
1/1
✓ Branch 22 → 23 taken 25 times.
25 SecureUtil::zeroMemory(prkKey);
107
1/1
✓ Branch 23 → 24 taken 25 times.
25 SecureUtil::zeroMemory(ikm);
108
1/1
✓ Branch 24 → 25 taken 25 times.
25 SecureUtil::zeroMemory(prk);
109 25 return keys;
110 25 }
111
112 25 void wipe(ContentKeys &keys) {
113 25 SecureUtil::zeroMemory(keys.cek);
114 25 SecureUtil::zeroMemory(keys.nonce);
115 25 }
116
117 25 std::optional<QByteArray> aesGcm(const Api *a, bool encrypting, const ContentKeys &keys, const QByteArray &input,
118 QByteArray *tag) {
119
1/1
✓ Branch 2 → 3 taken 25 times.
25 std::unique_ptr<EvpCipherCtx, CipherDeleter> ctx(a->EVP_CIPHER_CTX_new(), {a});
120 25 const auto *key = reinterpret_cast<const unsigned char *>(keys.cek.constData());
121 25 const auto *iv = reinterpret_cast<const unsigned char *>(keys.nonce.constData());
122
2/2
✓ Branch 6 → 7 taken 10 times.
✓ Branch 6 → 8 taken 15 times.
25 auto init = encrypting ? a->EVP_EncryptInit_ex : a->EVP_DecryptInit_ex;
123
2/2
✓ Branch 9 → 10 taken 10 times.
✓ Branch 9 → 11 taken 15 times.
25 auto update = encrypting ? a->EVP_EncryptUpdate : a->EVP_DecryptUpdate;
124
2/2
✓ Branch 12 → 13 taken 10 times.
✓ Branch 12 → 14 taken 15 times.
25 auto finish = encrypting ? a->EVP_EncryptFinal_ex : a->EVP_DecryptFinal_ex;
125
3/4
✓ Branch 17 → 18 taken 25 times.
✓ Branch 19 → 20 taken 25 times.
✓ Branch 20 → 21 taken 25 times.
✗ Branch 20 → 28 not taken.
50 if (!ctx || init(ctx.get(), a->EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1 ||
126
4/7
✓ Branch 16 → 17 taken 25 times.
✗ Branch 16 → 28 not taken.
✓ Branch 23 → 24 taken 25 times.
✓ Branch 24 → 25 taken 25 times.
✗ Branch 24 → 28 not taken.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 25 times.
75 a->EVP_CIPHER_CTX_ctrl(ctx.get(), CtrlAeadSetIvLength, keys.nonce.size(), nullptr) != 1 ||
127
2/3
✓ Branch 26 → 27 taken 25 times.
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 25 times.
25 init(ctx.get(), nullptr, nullptr, key, iv) != 1)
128 ✗ return std::nullopt;
129
1/1
✓ Branch 33 → 34 taken 25 times.
25 QByteArray output(input.size() + kTagBytes, Qt::Uninitialized);
130 25 int written = 0;
131
2/2
✓ Branch 36 → 37 taken 25 times.
✓ Branch 38 → 39 taken 25 times.
25 if (update(ctx.get(), reinterpret_cast<unsigned char *>(output.data()), &written,
132
1/2
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 41 taken 25 times.
50 reinterpret_cast<const unsigned char *>(input.constData()), input.size()) != 1)
133 ✗ return std::nullopt;
134
3/4
✓ Branch 41 → 42 taken 15 times.
✓ Branch 41 → 47 taken 10 times.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 50 taken 25 times.
40 if (!encrypting &&
135
3/4
✓ Branch 42 → 43 taken 15 times.
✓ Branch 44 → 45 taken 15 times.
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 15 times.
15 a->EVP_CIPHER_CTX_ctrl(ctx.get(), CtrlAeadSetTag, kTagBytes, tag->data()) != 1)
136 ✗ return std::nullopt;
137 25 int finalWritten = 0;
138
4/4
✓ Branch 50 → 51 taken 25 times.
✓ Branch 52 → 53 taken 25 times.
✓ Branch 53 → 54 taken 3 times.
✓ Branch 53 → 55 taken 22 times.
25 if (finish(ctx.get(), reinterpret_cast<unsigned char *>(output.data()) + written, &finalWritten) != 1)
139 3 return std::nullopt; // decryption: the tag did not verify
140
1/1
✓ Branch 55 → 56 taken 22 times.
22 output.truncate(written + finalWritten);
141
2/2
✓ Branch 56 → 57 taken 10 times.
✓ Branch 56 → 63 taken 12 times.
22 if (encrypting) {
142
1/1
✓ Branch 57 → 58 taken 10 times.
10 tag->resize(kTagBytes);
143
3/4
✓ Branch 58 → 59 taken 10 times.
✓ Branch 60 → 61 taken 10 times.
✗ Branch 61 → 62 not taken.
✓ Branch 61 → 63 taken 10 times.
10 if (a->EVP_CIPHER_CTX_ctrl(ctx.get(), CtrlAeadGetTag, kTagBytes, tag->data()) != 1) return std::nullopt;
144 }
145 22 return output;
146 25 }
147
148 } // namespace
149
150 92 bool available(QString *error) {
151 92 return api(error) != nullptr;
152 }
153
154 68 std::optional<QByteArray> decodeBase64Url(const QString &text) {
155
4/7
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 15 taken 64 times.
✓ Branch 4 → 5 taken 4 times.
✗ Branch 4 → 15 not taken.
✓ Branch 9 → 10 taken 4 times.
✗ Branch 48 → 49 not taken.
✗ Branch 48 → 50 not taken.
72 static const QRegularExpression alphabet(QStringLiteral("^[A-Za-z0-9_-]*$"));
156
1/1
✓ Branch 15 → 16 taken 68 times.
68 const QString trimmed = text.trimmed();
157
9/12
✓ Branch 17 → 18 taken 68 times.
✓ Branch 18 → 19 taken 68 times.
✓ Branch 19 → 20 taken 66 times.
✓ Branch 19 → 22 taken 2 times.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 65 times.
✓ Branch 24 → 25 taken 68 times.
✗ Branch 24 → 26 not taken.
✓ Branch 26 → 27 taken 3 times.
✓ Branch 26 → 28 taken 65 times.
✗ Branch 51 → 52 not taken.
✗ Branch 51 → 53 not taken.
68 if (!alphabet.match(trimmed).hasMatch() || trimmed.size() % 4 == 1) return std::nullopt;
158 const auto decoded = QByteArray::fromBase64Encoding(
159
1/1
✓ Branch 30 → 31 taken 65 times.
195 trimmed.toLatin1(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals |
160
1/1
✓ Branch 31 → 32 taken 65 times.
130 QByteArray::AbortOnBase64DecodingErrors);
161
1/2
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 65 times.
65 if (!decoded) return std::nullopt;
162 65 return decoded.decoded;
163 68 }
164
165 245 QString encodeBase64Url(const QByteArray &bytes) {
166
2/2
✓ Branch 3 → 4 taken 245 times.
✓ Branch 4 → 5 taken 245 times.
245 return QString::fromLatin1(bytes.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
167 }
168
169 48 std::optional<Keys> generateKeys(QString *error) {
170
1/1
✓ Branch 2 → 3 taken 48 times.
48 const Api *a = api(error);
171
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 48 times.
48 if (!a) return std::nullopt;
172
1/1
✓ Branch 5 → 6 taken 48 times.
48 std::unique_ptr<EvpPkeyCtx, CtxDeleter> ctx(a->EVP_PKEY_CTX_new_from_name(nullptr, "EC", nullptr), {a});
173 48 EvpPkey *raw = nullptr;
174
6/10
✓ Branch 8 → 9 taken 48 times.
✗ Branch 8 → 18 not taken.
✓ Branch 10 → 11 taken 48 times.
✓ Branch 11 → 12 taken 48 times.
✗ Branch 11 → 18 not taken.
✓ Branch 13 → 14 taken 48 times.
✓ Branch 14 → 15 taken 48 times.
✗ Branch 14 → 18 not taken.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 28 taken 48 times.
96 if (!ctx || a->EVP_PKEY_keygen_init(ctx.get()) <= 0 || a->EVP_PKEY_CTX_set_group_name(ctx.get(), "P-256") <= 0 ||
175
2/3
✓ Branch 16 → 17 taken 48 times.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 48 times.
48 a->EVP_PKEY_keygen(ctx.get(), &raw) <= 0) {
176 ✗ setError(error, QStringLiteral("P-256 key generation failed"));
177 ✗ return std::nullopt;
178 }
179 48 Pkey key(raw, {a});
180 48 Keys keys;
181
1/1
✓ Branch 30 → 31 taken 48 times.
48 keys.privateKeyDer = privateDer(a, key.get());
182
1/1
✓ Branch 34 → 35 taken 48 times.
48 keys.publicKey = publicPoint(a, key.get());
183
1/1
✓ Branch 37 → 38 taken 48 times.
48 keys.authSecret.resize(kAuthSecretBytes);
184
4/5
✓ Branch 38 → 39 taken 48 times.
✗ Branch 38 → 60 not taken.
✓ Branch 39 → 40 taken 48 times.
✓ Branch 40 → 41 taken 48 times.
✓ Branch 41 → 42 taken 48 times.
48 QRandomGenerator::system()->generate(keys.authSecret.begin(), keys.authSecret.end());
185
1/2
✗ Branch 43 → 44 not taken.
✓ Branch 43 → 51 taken 48 times.
48 if (!keys.isValid()) {
186 ✗ setError(error, QStringLiteral("P-256 key encoding failed"));
187 ✗ return std::nullopt;
188 }
189 48 return keys;
190 48 }
191
192 6 std::optional<QByteArray> privateKeyFromRaw(const QByteArray &scalar, const QByteArray &publicKey) {
193
1/1
✓ Branch 2 → 3 taken 6 times.
6 const Api *a = api();
194
4/8
✓ Branch 3 → 4 taken 6 times.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 6 times.
✗ Branch 5 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 6 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 6 times.
6 if (!a || scalar.size() != 32 || publicKey.size() != kPublicKeyBytes) return std::nullopt;
195 // SEC 1 ECPrivateKey with the prime256v1 parameters and the public point.
196
5/5
✓ Branch 14 → 15 taken 6 times.
✓ Branch 15 → 16 taken 6 times.
✓ Branch 16 → 17 taken 6 times.
✓ Branch 17 → 18 taken 6 times.
✓ Branch 18 → 19 taken 6 times.
12 const QByteArray sec1 = QByteArray::fromHex("307702010104") + QByteArray(1, '\x20') + scalar +
197
4/4
✓ Branch 12 → 13 taken 6 times.
✓ Branch 13 → 14 taken 6 times.
✓ Branch 19 → 20 taken 6 times.
✓ Branch 20 → 21 taken 6 times.
18 QByteArray::fromHex("a00a06082a8648ce3d030107a144034200") + publicKey;
198
1/1
✓ Branch 29 → 30 taken 6 times.
6 Pkey key = privateKeyFromDer(a, sec1);
199
5/11
✓ Branch 31 → 32 taken 6 times.
✗ Branch 31 → 36 not taken.
✓ Branch 33 → 34 taken 6 times.
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 37 taken 6 times.
✓ Branch 38 → 39 taken 6 times.
✗ Branch 38 → 40 not taken.
✗ Branch 40 → 41 not taken.
✓ Branch 40 → 42 taken 6 times.
✗ Branch 75 → 76 not taken.
✗ Branch 75 → 77 not taken.
6 if (!key || publicPoint(a, key.get()) != publicKey) return std::nullopt;
200
1/1
✓ Branch 43 → 44 taken 6 times.
6 return privateDer(a, key.get());
201 6 }
202
203 24 std::optional<QByteArray> decrypt(const QByteArray &body, const Keys &keys, QString *error) {
204
1/1
✓ Branch 2 → 3 taken 24 times.
24 const Api *a = api(error);
205
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 24 times.
24 if (!a) return std::nullopt;
206
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 14 taken 23 times.
24 if (!keys.isValid()) {
207 1 setError(error, QStringLiteral("subscription keys incomplete"));
208 1 return std::nullopt;
209 }
210
2/2
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 26 taken 22 times.
23 if (body.size() > kMaxEncryptedBytes) {
211
1/1
✓ Branch 20 → 21 taken 1 time.
2 setError(error, QStringLiteral("payload exceeds %1 bytes").arg(kMaxEncryptedBytes));
212 1 return std::nullopt;
213 }
214
2/2
✓ Branch 27 → 28 taken 2 times.
✓ Branch 27 → 35 taken 20 times.
22 if (body.size() < kHeaderBytes) {
215 2 setError(error, QStringLiteral("payload truncated (header)"));
216 2 return std::nullopt;
217 }
218
1/1
✓ Branch 35 → 36 taken 20 times.
20 const QByteArray salt = body.left(kSaltBytes);
219 20 const quint32 recordSize = (quint32(quint8(body[16])) << 24) | (quint32(quint8(body[17])) << 16) |
220 20 (quint32(quint8(body[18])) << 8) | quint32(quint8(body[19]));
221 20 const int idLength = quint8(body[20]);
222
6/6
✓ Branch 41 → 42 taken 18 times.
✓ Branch 41 → 44 taken 2 times.
✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 45 taken 17 times.
✓ Branch 46 → 47 taken 3 times.
✓ Branch 46 → 54 taken 17 times.
20 if (idLength != kPublicKeyBytes || body.size() < kHeaderBytes + idLength + kTagBytes + 1) {
223 3 setError(error, QStringLiteral("payload truncated or sender key id is not a P-256 point"));
224 3 return std::nullopt;
225 }
226
1/1
✓ Branch 54 → 55 taken 17 times.
17 const QByteArray senderPublic = body.mid(kHeaderBytes, idLength);
227
1/1
✓ Branch 55 → 56 taken 17 times.
17 const QByteArray record = body.mid(kHeaderBytes + idLength);
228 // RFC 8291 §4: a push message is a single record.
229
5/6
✓ Branch 56 → 57 taken 17 times.
✗ Branch 56 → 59 not taken.
✓ Branch 58 → 59 taken 1 time.
✓ Branch 58 → 60 taken 16 times.
✓ Branch 61 → 62 taken 1 time.
✓ Branch 61 → 69 taken 16 times.
17 if (recordSize < 18 || quint32(record.size()) > recordSize) {
230 1 setError(error, QStringLiteral("unsupported record layout"));
231 1 return std::nullopt;
232 }
233
1/1
✓ Branch 69 → 70 taken 16 times.
16 Pkey own = privateKeyFromDer(a, keys.privateKeyDer);
234
1/1
✓ Branch 70 → 71 taken 16 times.
16 Pkey peer = publicKeyFromPoint(a, senderPublic);
235
1/2
✗ Branch 72 → 73 not taken.
✓ Branch 72 → 80 taken 16 times.
16 if (!own) {
236 ✗ setError(error, QStringLiteral("subscription private key unreadable"));
237 ✗ return std::nullopt;
238 }
239
2/2
✓ Branch 81 → 82 taken 1 time.
✓ Branch 81 → 89 taken 15 times.
16 if (!peer) {
240 1 setError(error, QStringLiteral("sender public key is not a valid P-256 point"));
241 1 return std::nullopt;
242 }
243
1/1
✓ Branch 91 → 92 taken 15 times.
15 QByteArray secret = ecdh(a, own.get(), peer.get());
244
1/2
✗ Branch 93 → 94 not taken.
✓ Branch 93 → 101 taken 15 times.
15 if (secret.isEmpty()) {
245 ✗ setError(error, QStringLiteral("key agreement failed"));
246 ✗ return std::nullopt;
247 }
248
1/1
✓ Branch 101 → 102 taken 15 times.
15 ContentKeys contentKeys = deriveKeys(secret, keys.authSecret, keys.publicKey, senderPublic, salt);
249
1/1
✓ Branch 102 → 103 taken 15 times.
15 SecureUtil::zeroMemory(secret);
250
1/1
✓ Branch 103 → 104 taken 15 times.
15 QByteArray tag = record.right(kTagBytes);
251
2/2
✓ Branch 105 → 106 taken 15 times.
✓ Branch 106 → 107 taken 15 times.
15 const auto plain = aesGcm(a, false, contentKeys, record.left(record.size() - kTagBytes), &tag);
252
1/1
✓ Branch 108 → 109 taken 15 times.
15 wipe(contentKeys);
253
2/2
✓ Branch 110 → 111 taken 3 times.
✓ Branch 110 → 118 taken 12 times.
15 if (!plain) {
254 3 setError(error, QStringLiteral("authentication tag mismatch (wrong key or manipulated payload)"));
255 3 return std::nullopt;
256 }
257 // Last record: data, delimiter 0x02, then zero padding (RFC 8188 §2).
258 12 qsizetype end = plain->size();
259
5/6
✓ Branch 122 → 123 taken 52 times.
✗ Branch 122 → 127 not taken.
✓ Branch 125 → 126 taken 40 times.
✓ Branch 125 → 127 taken 12 times.
✓ Branch 128 → 121 taken 40 times.
✓ Branch 128 → 129 taken 12 times.
52 while (end > 0 && plain->at(end - 1) == '\0') --end;
260
5/6
✓ Branch 129 → 130 taken 12 times.
✗ Branch 129 → 133 not taken.
✓ Branch 132 → 133 taken 1 time.
✓ Branch 132 → 134 taken 11 times.
✓ Branch 135 → 136 taken 1 time.
✓ Branch 135 → 143 taken 11 times.
12 if (end == 0 || plain->at(end - 1) != '\x02') {
261 1 setError(error, QStringLiteral("invalid record padding"));
262 1 return std::nullopt;
263 }
264
1/2
✗ Branch 143 → 144 not taken.
✓ Branch 143 → 154 taken 11 times.
11 if (end - 1 > kMaxPlaintextBytes) {
265 ✗ setError(error, QStringLiteral("decrypted message exceeds %1 bytes").arg(kMaxPlaintextBytes));
266 ✗ return std::nullopt;
267 }
268
1/1
✓ Branch 155 → 156 taken 11 times.
11 return plain->left(end - 1);
269 20 }
270
271 10 std::optional<QByteArray> encrypt(const QByteArray &plaintext, const QByteArray &receiverPublicKey,
272 const QByteArray &authSecret, QString *error, const QByteArray &salt,
273 const QByteArray &senderPrivateKeyDer) {
274
1/1
✓ Branch 2 → 3 taken 10 times.
10 const Api *a = api(error);
275
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 10 times.
10 if (!a) return std::nullopt;
276
1/1
✓ Branch 5 → 6 taken 10 times.
10 Pkey peer = publicKeyFromPoint(a, receiverPublicKey);
277
4/8
✓ Branch 7 → 8 taken 10 times.
✗ Branch 7 → 12 not taken.
✓ Branch 9 → 10 taken 10 times.
✗ Branch 9 → 12 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 10 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 22 taken 10 times.
10 if (!peer || authSecret.size() != kAuthSecretBytes || plaintext.size() > kMaxPlaintextBytes) {
278 ✗ setError(error, QStringLiteral("invalid receiver key, auth secret or plaintext size"));
279 ✗ return std::nullopt;
280 }
281 10 QByteArray senderDer = senderPrivateKeyDer;
282
2/2
✓ Branch 24 → 25 taken 9 times.
✓ Branch 24 → 38 taken 1 time.
10 if (senderDer.isEmpty()) {
283
1/1
✓ Branch 25 → 26 taken 9 times.
9 const auto generated = generateKeys(error);
284
1/2
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 30 taken 9 times.
9 if (!generated) return std::nullopt;
285 9 senderDer = generated->privateKeyDer;
286 9 }
287
1/1
✓ Branch 38 → 39 taken 10 times.
10 Pkey sender = privateKeyFromDer(a, senderDer);
288
2/3
✓ Branch 40 → 41 taken 10 times.
✗ Branch 40 → 43 not taken.
✓ Branch 42 → 44 taken 10 times.
10 const QByteArray senderPublic = sender ? publicPoint(a, sender.get()) : QByteArray();
289 10 QByteArray usedSalt = salt;
290
2/2
✓ Branch 46 → 47 taken 9 times.
✓ Branch 46 → 52 taken 1 time.
10 if (usedSalt.isEmpty()) {
291
1/1
✓ Branch 47 → 48 taken 9 times.
9 usedSalt.resize(kSaltBytes);
292
4/5
✓ Branch 48 → 49 taken 9 times.
✗ Branch 48 → 141 not taken.
✓ Branch 49 → 50 taken 9 times.
✓ Branch 50 → 51 taken 9 times.
✓ Branch 51 → 52 taken 9 times.
9 QRandomGenerator::system()->generate(usedSalt.begin(), usedSalt.end());
293 }
294
2/3
✓ Branch 53 → 54 taken 10 times.
✗ Branch 53 → 57 not taken.
✓ Branch 56 → 58 taken 10 times.
10 QByteArray secret = sender ? ecdh(a, sender.get(), peer.get()) : QByteArray();
295
4/8
✓ Branch 59 → 60 taken 10 times.
✗ Branch 59 → 64 not taken.
✓ Branch 61 → 62 taken 10 times.
✗ Branch 61 → 64 not taken.
✗ Branch 63 → 64 not taken.
✓ Branch 63 → 65 taken 10 times.
✗ Branch 66 → 67 not taken.
✓ Branch 66 → 74 taken 10 times.
10 if (secret.isEmpty() || senderPublic.isEmpty() || usedSalt.size() != kSaltBytes) {
296 ✗ setError(error, QStringLiteral("sender key agreement failed"));
297 ✗ return std::nullopt;
298 }
299
1/1
✓ Branch 74 → 75 taken 10 times.
10 ContentKeys keys = deriveKeys(secret, authSecret, receiverPublicKey, senderPublic, usedSalt);
300
1/1
✓ Branch 75 → 76 taken 10 times.
10 SecureUtil::zeroMemory(secret);
301 10 QByteArray tag;
302
3/3
✓ Branch 76 → 77 taken 10 times.
✓ Branch 77 → 78 taken 10 times.
✓ Branch 78 → 79 taken 10 times.
10 const auto cipher = aesGcm(a, true, keys, plaintext + QByteArray(1, '\x02'), &tag);
303
1/1
✓ Branch 81 → 82 taken 10 times.
10 wipe(keys);
304
1/2
✗ Branch 83 → 84 not taken.
✓ Branch 83 → 91 taken 10 times.
10 if (!cipher) {
305 ✗ setError(error, QStringLiteral("encryption failed"));
306 ✗ return std::nullopt;
307 }
308 // S106-083: the single record must fit rs (RFC 8291 §4); a fixed 4096
309 // made bodies above 4079 plaintext bytes invalid.
310 10 const quint32 recordSize = qMax<quint32>(4096, quint32(cipher->size() + tag.size()));
311 10 QByteArray header = usedSalt;
312
3/3
✓ Branch 96 → 97 taken 10 times.
✓ Branch 97 → 98 taken 10 times.
✓ Branch 98 → 99 taken 10 times.
10 header.append(char(recordSize >> 24)).append(char(recordSize >> 16)).append(char(recordSize >> 8))
313
1/1
✓ Branch 99 → 100 taken 10 times.
10 .append(char(recordSize & 0xff));
314
2/2
✓ Branch 100 → 101 taken 10 times.
✓ Branch 101 → 102 taken 10 times.
10 header.append(char(kPublicKeyBytes)).append(senderPublic);
315
2/2
✓ Branch 103 → 104 taken 10 times.
✓ Branch 104 → 105 taken 10 times.
10 return header + *cipher + tag;
316 10 }
317
318 } // namespace WebPushPayloadCodec
319