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 |