VLink  2.0.0
A high-performance communication middleware
security.h
浏览该文件的文档.
1 /*
2  * Copyright (C) 2026 by Thun Lu. All rights reserved.
3  * Author: Thun Lu <thun.lu@zohomail.cn>
4  * Repo: https://github.com/thun-res/vlink
5  * _ __ __ _ __
6  * | | / / / / (_) ____ / /__
7  * | | / / / / / / / __ \ / //_/
8  * | |/ / / /___ / / / / / / / ,<
9  * |___/ /_____/ /_/ /_/ /_/ /_/|_|
10  *
11  * Licensed under the Apache License, Version 2.0 (the "License");
12  * you may not use this file except in compliance with the License.
13  * You may obtain a copy of the License at
14  *
15  * http://www.apache.org/licenses/LICENSE-2.0
16  *
17  * Unless required by applicable law or agreed to in writing, software
18  * distributed under the License is distributed on an "AS IS" BASIS,
19  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20  * See the License for the specific language governing permissions and
21  * limitations under the License.
22  */
23 
24 /**
25  * @file security.h
26  * @brief Application-layer authenticated encryption with symmetric, hybrid asymmetric, and pluggable backends.
27  *
28  * @details
29  * @c Security sits between VLink serialisation and any transport layer. Every endpoint
30  * (publisher / subscriber / client / server / setter / getter) may own at most one @c Security
31  * instance; the instance authenticates and encrypts each outbound message and verifies and
32  * decrypts each inbound message before the transport sees the payload. It is fully orthogonal
33  * to the channel-level TLS controlled by @c SslOptions and can be combined with it.
34  *
35  * Compiling with @c VLINK_ENABLE_SECURITY enables the OpenSSL-backed default crypto suite.
36  * Without that macro only the user-supplied callback path is available.
37  *
38  * @par Encryption algorithms
39  *
40  * | Algorithm | Key size | Mode / construction | Used for |
41  * | ------------------ | ------------------ | ---------------------------------------- | --------------------------- |
42  * | AES-GCM | 128 bit | AEAD, 12-byte nonce, 16-byte tag | bulk encrypt / decrypt |
43  * | RSA-OAEP-SHA256 | >= 2048 b | wrap a fresh per-message AES key | asymmetric session-key wrap |
44  * | RSA-PSS-SHA256 | >= 2048 b | sender signature over AAD-bound envelope | optional sender identity |
45  * | SHA-256 truncation | 256 -> 128 | derive raw @c key -> AES-128 key | symmetric key derivation |
46  * | PBKDF2-HMAC-SHA256 | configurable iters | derive passphrase -> AES-128 | symmetric key derivation |
47  *
48  * @par Key sources
49  *
50  * | Source | Selector field(s) | Notes |
51  * | -------------------------- | ----------------------------------------- | ---------------------------------------- |
52  * | Built-in default symmetric | every cryptographic field empty | only with @c VLINK_ENABLE_SECURITY |
53  * | Explicit raw key | @c Config::key | SHA-256 truncated to AES-128 |
54  * | Passphrase + salt | @c Config::passphrase + @c pbkdf2_salt | PBKDF2-HMAC-SHA256 @ iterations |
55  * | Peer public-key PEM | @c Config::public_key_pem | RSA-OAEP wrap of a fresh session key |
56  * | Local private-key PEM | @c Config::private_key_pem | RSA-OAEP unwrap of session key |
57  * | Custom callback pair | @c encrypt_callback + @c decrypt_callback | bypasses every built-in algorithm |
58  *
59  * @par Mode summary diagram
60  * @code
61  * sender transport receiver
62  * +------------+ +-----------+ +-------------+
63  * | plaintext | ---> [select mode] ---> AEAD/RSA-OAEP envelope ---> bytes on wire ---> [select mode] ---> |
64  * +------------+ ^ ^ plaintext |
65  * | | |
66  * custom > asymmetric > symmetric custom > asymmetric > symmetric |
67  * |
68  * encrypt(in, out) --> envelope(version, mode, nonce, AAD) --> ciphertext + tag --> decrypt(in, out) v
69  * @endcode
70  *
71  * @par Example (1: custom callback pair)
72  * @code
73  * vlink::Security::Config cfg;
74  * cfg.encrypt_callback = [](const vlink::Bytes& in, vlink::Bytes& out) {
75  * out = my_aead_encrypt(in);
76  * return !out.empty();
77  * };
78  * cfg.decrypt_callback = [](const vlink::Bytes& in, vlink::Bytes& out) {
79  * out = my_aead_decrypt(in);
80  * return !out.empty();
81  * };
82  * vlink::Security sec(std::move(cfg));
83  * @endcode
84  *
85  * @par Example (2: symmetric passphrase with PBKDF2)
86  * @code
87  * vlink::Security::Config cfg;
88  * cfg.passphrase = "correct horse battery staple";
89  * cfg.pbkdf2_salt = shared_salt; // >= 16 bytes, shared out of band
90  * cfg.pbkdf2_iterations = 200000U;
91  * vlink::Security sec(cfg);
92  * @endcode
93  *
94  * @par Example (3: asymmetric PEM with optional sender authentication)
95  * @code
96  * auto sender_cfg = vlink::Security::from_public_key_path("peer_pub.pem");
97  * sender_cfg.advanced.signing_key_pem = own_priv_pem; // optional RSA-PSS signature
98  * vlink::Security sender(std::move(sender_cfg));
99  *
100  * auto receiver_cfg = vlink::Security::from_private_key_path("own_priv.pem");
101  * receiver_cfg.advanced.verify_key_pem = peer_pub_pem; // reject unsigned envelopes
102  * vlink::Security receiver(std::move(receiver_cfg));
103  * @endcode
104  *
105  * @note
106  * - Every public method is thread-safe; concurrent @c encrypt() / @c decrypt() are serialised
107  * by an internal mutex.
108  * - Configuration is immutable after construction; rebuild the @c Security instance to change
109  * keys or callbacks.
110  * - When @c VLINK_ENABLE_SECURITY is undefined only the callback path is usable; other fields
111  * are accepted but emit a warning and remain unconfigured.
112  */
113 
114 #pragma once
115 
116 #include <cstdint>
117 #include <memory>
118 #include <string>
119 
120 #include "../base/bytes.h"
121 #include "../base/functional.h"
122 #include "../base/macros.h"
123 
124 namespace vlink {
125 
126 /**
127  * @class Security
128  * @brief Thread-safe authenticated encryption with symmetric, asymmetric, and pluggable modes.
129  *
130  * @details
131  * One @c Security instance per endpoint. Configuration is supplied through @c Config at
132  * construction time; copy and assignment are deleted, move is supported. Each call to
133  * @c encrypt() / @c decrypt() selects a mode using the precedence order described in the
134  * file-level @c mode summary diagram.
135  */
136 class VLINK_EXPORT Security final {
137  public:
138  /**
139  * @brief Callable signature for user-supplied encrypt / decrypt callbacks.
140  *
141  * @details
142  * Implementations must populate @c out and return @c true on success; failure leaves
143  * @c out empty and the surrounding @c encrypt() / @c decrypt() call returns @c false.
144  * @c Function is copyable, so callbacks may be passed in a const-reference @c Config.
145  */
146  using Callback = Function<bool(const Bytes& in, Bytes& out)>;
147 
148  /**
149  * @struct Config
150  * @brief Aggregate of every parameter accepted by the @c Security constructor.
151  *
152  * @details
153  * Fields are processed independently. Empty strings, empty @c Bytes, and null callbacks
154  * mean "leave this slot blank"; non-empty values are validated by the constructor and
155  * installed on success or logged-and-ignored on failure. When every cryptographic field
156  * is empty and @c VLINK_ENABLE_SECURITY is defined, the constructor falls back to the
157  * built-in default symmetric slot, which is intended for development only.
158  *
159  * @par Precedence rules
160  * - When both callbacks are present, the built-in path is bypassed for both directions.
161  * - Outbound: @c public_key_pem (if installed) -> @c key / @c passphrase -> default slot.
162  * - Inbound : @c private_key_pem (if installed) -> @c key / @c passphrase -> default slot.
163  */
164  struct Config final {
165  /**
166  * @struct Advanced
167  * @brief Low-frequency policy knobs and sender-authentication keys.
168  */
169  struct Advanced final {
170  std::string aad_context; ///< Application or channel tag (<= 65535 bytes) bound into AEAD AAD.
171  uint32_t replay_window{4096U}; ///< Sliding replay-window size in messages; @c 0 disables anti-replay.
172  std::string signing_key_pem; ///< Local RSA private key (PEM) used to sign with RSA-PSS-SHA256.
173  std::string verify_key_pem; ///< Peer's RSA public key (PEM) required for RSA-PSS verification.
174  };
175 
176  std::string key; ///< Raw symmetric seed; SHA-256 truncated to 16 bytes.
177  std::string passphrase; ///< Low-entropy passphrase consumed by PBKDF2-HMAC-SHA256.
178  Bytes pbkdf2_salt; ///< Per-deployment salt (>=16 bytes) shared out of band.
179  uint32_t pbkdf2_iterations{200000U}; ///< PBKDF2 iteration count, tune for target hardware.
180  std::string public_key_pem; ///< Peer's RSA public key (PEM) for RSA-OAEP outbound wrap.
181  std::string private_key_pem; ///< Local RSA private key (PEM) for RSA-OAEP inbound unwrap.
182  Callback encrypt_callback; ///< Custom encrypt; bypasses the built-in AEAD pipeline.
183  Callback decrypt_callback; ///< Custom decrypt; must accompany @c encrypt_callback.
184  Advanced advanced; ///< AAD, replay window, and signing / verifying PEM material.
185 
186  Config() = default;
187  };
188 
189  /**
190  * @brief Loads a private-key PEM file into a fresh @c Config.
191  *
192  * @details
193  * Reads the file at construction time and populates @c Config::private_key_pem. RSA
194  * validation is deferred until the @c Security constructor consumes the config.
195  *
196  * @param private_key_path Filesystem path of the PEM-encoded private key.
197  * @return Pre-populated @c Config; @c private_key_pem is empty when the file is unreadable.
198  */
199  [[nodiscard]] static Config from_private_key_path(const std::string& private_key_path);
200 
201  /**
202  * @brief Loads a public-key PEM file into a fresh @c Config.
203  *
204  * @param public_key_path Filesystem path of the PEM-encoded public key.
205  * @return Pre-populated @c Config; @c public_key_pem is empty when the file is unreadable.
206  */
207  [[nodiscard]] static Config from_public_key_path(const std::string& public_key_path);
208 
209  /**
210  * @brief Loads both a public-key and a private-key PEM file into a fresh @c Config.
211  *
212  * @param public_key_path Filesystem path of the PEM-encoded peer public key.
213  * @param private_key_path Filesystem path of the PEM-encoded local private key.
214  * @return Pre-populated @c Config; per-file misses leave only the affected field empty.
215  */
216  [[nodiscard]] static Config from_key_paths(const std::string& public_key_path, const std::string& private_key_path);
217 
218  /**
219  * @brief Constructs an empty @c Security; equivalent to @c Security(Config{}).
220  *
221  * @details
222  * With built-in algorithms compiled in, this installs the default symmetric slot; otherwise
223  * the instance reports @c is_configured() == @c false and refuses to encrypt or decrypt.
224  */
226 
227  /**
228  * @brief Constructs from a configuration aggregate, copying caller state.
229  *
230  * @param cfg Configuration aggregate; every non-empty field is validated and installed.
231  */
232  explicit Security(const Config& cfg);
233 
234  /**
235  * @brief Constructs from a configuration aggregate, moving caller state in.
236  *
237  * @param cfg Configuration aggregate consumed during construction.
238  */
239  explicit Security(Config&& cfg);
240 
241  /**
242  * @brief Destroys the instance and zeroises any held symmetric key material in place.
243  */
245 
246  /**
247  * @brief Move-constructs from another @c Security; the source becomes default-constructed.
248  */
249  Security(Security&&) noexcept;
250 
251  /**
252  * @brief Move-assigns from another @c Security; the source becomes default-constructed.
253  */
254  Security& operator=(Security&&) noexcept;
255 
256  /**
257  * @brief Encrypts @p in into @p out using the highest-precedence active mode.
258  *
259  * @details
260  * The selected mode follows the precedence summarised in the file-level diagram:
261  * @c custom > @c asymmetric (public key present) > @c symmetric. Empty @p in fails;
262  * AEAD requires at least one byte of authenticated material. Inputs exceeding
263  * @c INT_MAX bytes are rejected.
264  *
265  * @param in Plaintext payload.
266  * @param out Output buffer overwritten on success and emptied on failure.
267  * @return @c true on success; @c false otherwise.
268  */
269  bool encrypt(const Bytes& in, Bytes& out);
270 
271  /**
272  * @brief Decrypts @p in into @p out using the highest-precedence active mode.
273  *
274  * @details
275  * Mode selection mirrors @c encrypt() but uses the inbound direction
276  * (@c custom > @c asymmetric private key > @c symmetric). Tampered ciphertext,
277  * mismatched AAD, replayed sequence numbers, and missing or invalid RSA-PSS
278  * signatures (when @c verify_key_pem is set) cause failure.
279  *
280  * @param in Ciphertext produced by a peer's @c encrypt().
281  * @param out Output buffer overwritten on success and emptied on failure.
282  * @return @c true on success; @c false otherwise.
283  */
284  bool decrypt(const Bytes& in, Bytes& out);
285 
286  /**
287  * @brief Reports whether at least one usable cryptographic slot is installed.
288  *
289  * @details
290  * A @c true result merely means the instance can call @c encrypt() or @c decrypt() in
291  * @b some direction; senders should additionally verify @c can_encrypt() and receivers
292  * @c can_decrypt() to catch direction-specific RSA misconfiguration.
293  *
294  * @return @c true when any slot is usable in either direction.
295  */
296  [[nodiscard]] bool is_configured() const noexcept;
297 
298  /**
299  * @brief Reports whether @c encrypt() will succeed.
300  *
301  * @details
302  * Requires at least one of: a custom callback pair, a derived AES-128 key, or a peer
303  * @c public_key_pem. A bare @c private_key_pem alone is insufficient.
304  *
305  * @return @c true when an encryption capability is installed.
306  */
307  [[nodiscard]] bool can_encrypt() const noexcept;
308 
309  /**
310  * @brief Reports whether @c decrypt() will succeed.
311  *
312  * @details
313  * Requires at least one of: a custom callback pair, a derived AES-128 key, or a local
314  * @c private_key_pem. A bare @c public_key_pem alone is insufficient.
315  *
316  * @return @c true when a decryption capability is installed.
317  */
318  [[nodiscard]] bool can_decrypt() const noexcept;
319 
320  private:
321  struct Impl;
322  std::unique_ptr<Impl> impl_;
323 
325 };
326 
327 } // namespace vlink
#define VLINK_EXPORT
Definition: macros.h:81
#define VLINK_DISALLOW_COPY_AND_ASSIGN(classname)
Deletes the copy constructor and copy-assignment operator of classname.
Definition: macros.h:174