VLink  2.1.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  /**
172  * Sliding replay-window size in messages; @c 0 disables anti-replay. Built-in replay protection separately
173  * tracks at most 1024 symmetric and 1024 asymmetric sender identities for the lifetime of a @c Security
174  * instance; a mode rejects previously unseen identities after its table reaches that limit.
175  */
176  uint32_t replay_window{4096U};
177  std::string signing_key_pem; ///< Local RSA private key (PEM) used to sign with RSA-PSS-SHA256.
178  std::string verify_key_pem; ///< Peer's RSA public key (PEM) required for RSA-PSS verification.
179  };
180 
181  std::string key; ///< Raw symmetric seed; SHA-256 truncated to 16 bytes.
182  std::string passphrase; ///< Low-entropy passphrase consumed by PBKDF2-HMAC-SHA256.
183  Bytes pbkdf2_salt; ///< Per-deployment salt (>=16 bytes) shared out of band.
184  uint32_t pbkdf2_iterations{200000U}; ///< PBKDF2 iteration count, tune for target hardware.
185  std::string public_key_pem; ///< Peer's RSA public key (PEM) for RSA-OAEP outbound wrap.
186  std::string private_key_pem; ///< Local RSA private key (PEM) for RSA-OAEP inbound unwrap.
187  Callback encrypt_callback; ///< Custom encrypt; bypasses the built-in AEAD pipeline.
188  Callback decrypt_callback; ///< Custom decrypt; must accompany @c encrypt_callback.
189  Advanced advanced; ///< AAD, replay window, and signing / verifying PEM material.
190 
191  Config() = default;
192  };
193 
194  /**
195  * @brief Loads a private-key PEM file into a fresh @c Config.
196  *
197  * @details
198  * Reads the file at construction time and populates @c Config::private_key_pem. RSA
199  * validation is deferred until the @c Security constructor consumes the config.
200  *
201  * @param private_key_path Filesystem path of the PEM-encoded private key.
202  * @return Pre-populated @c Config; @c private_key_pem is empty when the file is unreadable.
203  */
204  [[nodiscard]] static Config from_private_key_path(const std::string& private_key_path);
205 
206  /**
207  * @brief Loads a public-key PEM file into a fresh @c Config.
208  *
209  * @param public_key_path Filesystem path of the PEM-encoded public key.
210  * @return Pre-populated @c Config; @c public_key_pem is empty when the file is unreadable.
211  */
212  [[nodiscard]] static Config from_public_key_path(const std::string& public_key_path);
213 
214  /**
215  * @brief Loads both a public-key and a private-key PEM file into a fresh @c Config.
216  *
217  * @param public_key_path Filesystem path of the PEM-encoded peer public key.
218  * @param private_key_path Filesystem path of the PEM-encoded local private key.
219  * @return Pre-populated @c Config; per-file misses leave only the affected field empty.
220  */
221  [[nodiscard]] static Config from_key_paths(const std::string& public_key_path, const std::string& private_key_path);
222 
223  /**
224  * @brief Constructs an empty @c Security; equivalent to @c Security(Config{}).
225  *
226  * @details
227  * With built-in algorithms compiled in, this installs the default symmetric slot; otherwise
228  * the instance reports @c is_configured() == @c false and refuses to encrypt or decrypt.
229  */
231 
232  /**
233  * @brief Constructs from a configuration aggregate, copying caller state.
234  *
235  * @param cfg Configuration aggregate; every non-empty field is validated and installed.
236  */
237  explicit Security(const Config& cfg);
238 
239  /**
240  * @brief Constructs from a configuration aggregate, moving caller state in.
241  *
242  * @param cfg Configuration aggregate consumed during construction.
243  */
244  explicit Security(Config&& cfg);
245 
246  /**
247  * @brief Destroys the instance and zeroises any held symmetric key material in place.
248  */
250 
251  /**
252  * @brief Move-constructs from another @c Security; the source becomes default-constructed.
253  */
254  Security(Security&&) noexcept;
255 
256  /**
257  * @brief Move-assigns from another @c Security; the source becomes default-constructed.
258  */
259  Security& operator=(Security&&) noexcept;
260 
261  /**
262  * @brief Encrypts @p in into @p out using the highest-precedence active mode.
263  *
264  * @details
265  * The selected mode follows the precedence summarised in the file-level diagram:
266  * @c custom > @c asymmetric (public key present) > @c symmetric. Empty @p in fails;
267  * AEAD requires at least one byte of authenticated material. Inputs exceeding
268  * @c INT_MAX bytes are rejected.
269  *
270  * @param in Plaintext payload.
271  * @param out Output buffer overwritten on success and emptied on failure.
272  * @return @c true on success; @c false otherwise.
273  */
274  bool encrypt(const Bytes& in, Bytes& out);
275 
276  /**
277  * @brief Decrypts @p in into @p out using the highest-precedence active mode.
278  *
279  * @details
280  * Mode selection mirrors @c encrypt() but uses the inbound direction
281  * (@c custom > @c asymmetric private key > @c symmetric). Tampered ciphertext,
282  * mismatched AAD, replayed sequence numbers, and missing or invalid RSA-PSS
283  * signatures (when @c verify_key_pem is set) cause failure.
284  *
285  * @param in Ciphertext produced by a peer's @c encrypt().
286  * @param out Output buffer overwritten on success and emptied on failure.
287  * @return @c true on success; @c false otherwise.
288  */
289  bool decrypt(const Bytes& in, Bytes& out);
290 
291  /**
292  * @brief Reports whether at least one usable cryptographic slot is installed.
293  *
294  * @details
295  * A @c true result merely means the instance can call @c encrypt() or @c decrypt() in
296  * @b some direction; senders should additionally verify @c can_encrypt() and receivers
297  * @c can_decrypt() to catch direction-specific RSA misconfiguration.
298  *
299  * @return @c true when any slot is usable in either direction.
300  */
301  [[nodiscard]] bool is_configured() const noexcept;
302 
303  /**
304  * @brief Reports whether @c encrypt() will succeed.
305  *
306  * @details
307  * Requires at least one of: a custom callback pair, a derived AES-128 key, or a peer
308  * @c public_key_pem. A bare @c private_key_pem alone is insufficient.
309  *
310  * @return @c true when an encryption capability is installed.
311  */
312  [[nodiscard]] bool can_encrypt() const noexcept;
313 
314  /**
315  * @brief Reports whether @c decrypt() will succeed.
316  *
317  * @details
318  * Requires at least one of: a custom callback pair, a derived AES-128 key, or a local
319  * @c private_key_pem. A bare @c public_key_pem alone is insufficient.
320  *
321  * @return @c true when a decryption capability is installed.
322  */
323  [[nodiscard]] bool can_decrypt() const noexcept;
324 
325  private:
326  struct Impl;
327  std::unique_ptr<Impl> impl_;
328 
330 };
331 
332 } // 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