Stroika Library 3.0d16
 
Loading...
Searching...
No Matches
OpenSSLCryptoStream.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2025. All rights reserved
3 */
4#include "Stroika/Foundation/StroikaPreComp.h"
5
6#if qStroika_HasComponent_OpenSSL
7#include <openssl/err.h>
8#include <openssl/evp.h>
9#endif
10
11#include "Stroika/Foundation/Containers/Common.h"
13#include "Stroika/Foundation/Execution/Common.h"
15#include "Stroika/Foundation/Streams/InternallySynchronizedInputStream.h"
16#include "Stroika/Foundation/Streams/InternallySynchronizedOutputStream.h"
17
18#include "OpenSSLCryptoStream.h"
19
20using std::byte;
21
22using namespace Stroika::Foundation;
24using namespace Stroika::Foundation::Cryptography;
25using namespace Stroika::Foundation::Cryptography::Encoding;
26using namespace Stroika::Foundation::Memory;
27using namespace Stroika::Foundation::Streams;
28
29using Memory::BLOB;
30
31// @todo examine/test https://github.com/saju/misc/blob/master/misc/openssl_aes.c
32
33#if qStroika_HasComponent_OpenSSL && defined(_MSC_VER)
34// Use #pragma comment lib instead of explicit entry in the lib entry of the project file
35#if OPENSSL_VERSION_NUMBER < 0x1010000fL
36#pragma comment(lib, "libeay32.lib")
37#pragma comment(lib, "ssleay32.lib")
38#else
39#pragma comment(lib, "libcrypto.lib")
40#pragma comment(lib, "libssl.lib")
41#pragma comment(lib, "ws2_32.lib")
42#pragma comment(lib, "crypt32.lib")
43#endif
44#endif
45
46//// VERY ROUGH DRAFT - NOT VERY CLOSE TO CORRECT FOR ALL ALGORITHSM
47//// SEE http://www.openssl.org/docs/crypto/EVP_EncryptInit.html
48/// SEE https://wiki.openssl.org/index.php/EVP_Symmetric_Encryption_and_Decryption
49//// for details on what todo
50
51#if qStroika_HasComponent_OpenSSL
52
53namespace {
54 struct InOutStrmCommon_ {
55 InOutStrmCommon_ (const OpenSSLCryptoParams& cryptoParams, Direction d)
56 : fCTX_{::EVP_CIPHER_CTX_new ()}
57 {
58 try {
59 cryptoParams.fInitializer (fCTX_, d);
60 }
61 catch (...) {
62 ::EVP_CIPHER_CTX_free (fCTX_);
64 }
65 }
66 InOutStrmCommon_ (const InOutStrmCommon_&) = delete;
67 InOutStrmCommon_& operator= (const InOutStrmCommon_&) = delete;
68 virtual ~InOutStrmCommon_ ()
69 {
70 ::EVP_CIPHER_CTX_free (fCTX_);
71 }
72 static constexpr size_t _GetMinOutBufSize (size_t n)
73 {
74 return n + EVP_MAX_BLOCK_LENGTH;
75 }
76 // return nBytes in outBuf, throws on error
77 span<byte> _runOnce (span<const byte> data2Process, span<byte> outBuf)
78 {
79 Require (outBuf.size () >= _GetMinOutBufSize (data2Process.size ())); // always need out buf big enuf for inbuf
80 int outLen = 0;
81 Cryptography::Providers::OpenSSL::Exception::ThrowLastErrorIfFailed (
82 ::EVP_CipherUpdate (fCTX_, reinterpret_cast<unsigned char*> (outBuf.data ()), &outLen,
83 reinterpret_cast<const unsigned char*> (data2Process.data ()), static_cast<int> (data2Process.size ())));
84 Ensure (outLen >= 0);
85 Ensure (static_cast<size_t> (outLen) <= outBuf.size ());
86 return outBuf.subspan (0, outLen);
87 }
88 // return nBytes in outBuf, throws on error
89 // Can call multiple times - it keeps track itself if finalized.
90 size_t _cipherFinal (byte* outBufStart, [[maybe_unused]] byte* outBufEnd)
91 {
92 Require (outBufStart <= outBufEnd and static_cast<size_t> (outBufEnd - outBufStart) >= _GetMinOutBufSize (0));
93 if (fFinalCalled_) {
94 return 0; // not an error - just zero more bytes
95 }
96 int outLen = 0;
97 Cryptography::Providers::OpenSSL::Exception::ThrowLastErrorIfFailed (
98 ::EVP_CipherFinal_ex (fCTX_, reinterpret_cast<unsigned char*> (outBufStart), &outLen));
99 fFinalCalled_ = true;
100 Ensure (outLen >= 0);
101 Ensure (outLen <= (outBufEnd - outBufStart));
102 return size_t (outLen);
103 }
104 EVP_CIPHER_CTX* fCTX_{nullptr};
105 bool fFinalCalled_{false};
106 };
107}
108
109namespace {
110 class OpenSSLInputStreamRep_ : public InputStream::IRep<byte>, private InOutStrmCommon_ {
111 private:
112 static constexpr size_t kInBufSize_ = 10 * 1024;
113
114 public:
115 OpenSSLInputStreamRep_ (const OpenSSLCryptoParams& cryptoParams, Direction d, const InputStream::Ptr<byte>& realIn)
116 : InOutStrmCommon_{cryptoParams, d}
117 , fRealIn_{realIn}
118 {
119 }
120 virtual bool IsSeekable () const override
121 {
122 return false;
123 }
124 virtual void CloseRead () override
125 {
126 if (fRealIn_ != nullptr) {
127 fRealIn_.Close ();
128 }
129 Assert (fRealIn_ == nullptr);
130 Ensure (not IsOpenRead ());
131 }
132 virtual bool IsOpenRead () const override
133 {
134 return fRealIn_ != nullptr;
135 }
136 virtual SeekOffsetType GetReadOffset () const override
137 {
139 Require (IsOpenRead ());
140 return 0;
141 }
142 virtual optional<size_t> AvailableToRead () override
143 {
144 // must override since not seekable
145 if (not PreRead_ (NoDataAvailableHandling::eDontBlock)) {
146 return nullopt;
147 }
148 return static_cast<size_t> (fOutBufEnd_ > fOutBufStart_);
149 }
150 virtual optional<SeekOffsetType> RemainingLength () override
151 {
152 return nullopt; // generally cannot tell without side-effects on input stream
153 }
154 virtual optional<span<ElementType>> Read (span<ElementType> intoBuffer, NoDataAvailableHandling blockFlag) override
155 {
156 /*
157 * Keep track if unread bytes in fOutBuf_ - bounded by fOutBufStart_ and fOutBufEnd_.
158 * If none to read there - pull from fRealIn_ src, and push those through the cipher.
159 * and use that to re-populate fOutBuf_.
160 */
161 Require (not intoBuffer.empty ());
162 [[maybe_unused]] lock_guard critSec{fCriticalSection_};
163 Require (IsOpenRead ());
164 if (not PreRead_ (blockFlag)) {
165 return nullopt;
166 }
167 if (fOutBufStart_ < fOutBufEnd_) {
168 size_t n2Copy = min<size_t> (fOutBufEnd_ - fOutBufStart_, intoBuffer.size ());
169 (void)::memcpy (intoBuffer.data (), fOutBufStart_, n2Copy);
170 fOutBufStart_ += n2Copy;
171 return intoBuffer.subspan (0, n2Copy);
172 }
173 return span<ElementType>{}; // EOF
174 }
175 // false means EWouldBlock
176 bool PreRead_ (NoDataAvailableHandling blockFlag)
177 {
178 if (fOutBufStart_ == fOutBufEnd_) {
179 /*
180 * Then pull from 'real in' stream until we have reach EOF there, or until we have some bytes to output
181 * on our own.
182 */
183 byte toDecryptBuf[kInBufSize_];
184 Again:
185 size_t n2Decrypt = 0;
186 switch (blockFlag) {
187 case NoDataAvailableHandling::eBlockIfNoDataAvailable: {
188 n2Decrypt = fRealIn_.ReadBlocking (span{toDecryptBuf}).size ();
189 } break;
190 case NoDataAvailableHandling::eDontBlock: {
191 if (auto oRes = fRealIn_.ReadNonBlocking (span{toDecryptBuf})) {
192 n2Decrypt = oRes->size ();
193 }
194 else {
195 return false;
196 }
197 } break;
198 default:
200 }
201
202 if (n2Decrypt == 0) {
203 size_t nBytesInOutBuf = _cipherFinal (fOutBuf_.begin (), fOutBuf_.end ());
204 Assert (nBytesInOutBuf <= fOutBuf_.GetSize ());
205 fOutBufStart_ = fOutBuf_.begin ();
206 fOutBufEnd_ = fOutBufStart_ + nBytesInOutBuf;
207 }
208 else {
209 fOutBuf_.GrowToSize_uninitialized (_GetMinOutBufSize (NEltsOf (toDecryptBuf)));
210 span<byte> outBufUsed = _runOnce (span{toDecryptBuf, n2Decrypt}, span{fOutBuf_});
211 Assert (outBufUsed.size () <= fOutBuf_.GetSize ());
212 if (outBufUsed.size () == 0) {
213 // This can happen with block ciphers - we put stuff in, and get nothing out. But we cannot return EOF
214 // yet, so try again...
215 goto Again;
216 }
217 else {
218 fOutBufStart_ = fOutBuf_.begin ();
219 fOutBufEnd_ = fOutBufStart_ + outBufUsed.size ();
220 }
221 }
222 }
223 return true;
224 }
225
226 private:
227 mutable recursive_mutex fCriticalSection_;
228 Memory::InlineBuffer<byte, kInBufSize_ + EVP_MAX_BLOCK_LENGTH> fOutBuf_{_GetMinOutBufSize (kInBufSize_)};
229 byte* fOutBufStart_{nullptr};
230 byte* fOutBufEnd_{nullptr};
231 InputStream::Ptr<byte> fRealIn_;
232 };
233
234 class OpenSSLOutputStreamRep_ : public OutputStream::IRep<byte>, private InOutStrmCommon_ {
235 public:
236 OpenSSLOutputStreamRep_ (const OpenSSLCryptoParams& cryptoParams, Direction d, const OutputStream::Ptr<byte>& realOut)
237 : InOutStrmCommon_{cryptoParams, d}
238 , fRealOut_{realOut}
239 {
240 }
241 virtual ~OpenSSLOutputStreamRep_ ()
242 {
243 // no need for critical section because at most one thread can be running DTOR at a time, and no other methods can be running
244 try {
245 Flush ();
246 }
247 catch (...) {
248 // not great to do in DTOR, because we must drop exceptions on the floor!
249 }
250 }
251 virtual bool IsSeekable () const override
252 {
253 return false;
254 }
255 virtual void CloseWrite () override
256 {
257 if (fRealOut_ != nullptr) {
258 fRealOut_.Close ();
259 }
260 Ensure (fRealOut_ == nullptr);
261 }
262 virtual bool IsOpenWrite () const override
263 {
264 return fRealOut_ != nullptr;
265 }
266 virtual SeekOffsetType GetWriteOffset () const override
267 {
269 Require (IsOpenWrite ());
270 return 0;
271 }
272 virtual SeekOffsetType SeekWrite (Whence /*whence*/, SignedSeekOffsetType /*offset*/) override
273 {
275 Require (IsOpenWrite ());
276 return 0;
277 }
278 // pointer must refer to valid memory at least bufSize long, and cannot be nullptr. BufSize must always be >= 1.
279 // Writes always succeed fully or throw.
280 virtual void Write (span<const byte> elts) override
281 {
282 Require (not elts.empty ());
283 Require (IsOpenWrite ());
284 StackBuffer<byte, 1000 + EVP_MAX_BLOCK_LENGTH> outBuf{Memory::eUninitialized, _GetMinOutBufSize (elts.size ())};
285 [[maybe_unused]] auto&& critSec = lock_guard{fCriticalSection_};
286 span<byte> outBufUsed = _runOnce (elts, span{outBuf});
287 Assert (outBufUsed.size () <= outBuf.GetSize ());
288 fRealOut_.Write (outBufUsed);
289 }
290 virtual void Flush () override
291 {
292 Require (IsOpenWrite ());
293 byte outBuf[EVP_MAX_BLOCK_LENGTH]; // intentionally uninitialized
294 size_t nBytesInOutBuf = _cipherFinal (begin (outBuf), end (outBuf));
295 Assert (nBytesInOutBuf < sizeof (outBuf));
296 fRealOut_.Write (span{outBuf, nBytesInOutBuf});
297 }
298
299 private:
300 mutable recursive_mutex fCriticalSection_;
301 OutputStream::Ptr<byte> fRealOut_;
302 };
303}
304
305/*
306 ********************************************************************************
307 ******************** Cryptography::OpenSSLInputStream **************************
308 ********************************************************************************
309 */
310namespace {
311 void ApplySettings2CTX_ (EVP_CIPHER_CTX* ctx, const EVP_CIPHER* cipher, Direction d, bool nopad, bool useArgumentKeyLength,
312 const Memory::BLOB& key, const Memory::BLOB& initialIV)
313 {
314 RequireNotNull (ctx);
315 RequireNotNull (cipher);
316 bool enc = (d == Direction::eEncrypt);
317 if (nopad) {
318 Verify (::EVP_CIPHER_CTX_set_padding (ctx, 0) == 1);
319 }
320 Cryptography::Providers::OpenSSL::Exception::ThrowLastErrorIfFailed (::EVP_CipherInit_ex (ctx, cipher, NULL, nullptr, nullptr, enc));
321 size_t keyLen = ::EVP_CIPHER_CTX_key_length (ctx);
322 size_t ivLen = ::EVP_CIPHER_CTX_iv_length (ctx);
323
324 if (useArgumentKeyLength) {
325 keyLen = key.length ();
326 Verify (::EVP_CIPHER_CTX_set_key_length (ctx, static_cast<int> (keyLen)) == 1);
327 }
328
329 StackBuffer<byte> useKey{Memory::eUninitialized, keyLen};
330 StackBuffer<byte> useIV{Memory::eUninitialized, ivLen};
331
332 (void)::memset (useKey.begin (), 0, keyLen);
333 (void)::memset (useIV.begin (), 0, ivLen);
334
335 (void)::memcpy (useKey.begin (), key.begin (), min (keyLen, key.size ()));
336 if (not initialIV.empty ()) {
337 (void)::memcpy (useIV.begin (), initialIV.begin (), min (ivLen, initialIV.size ()));
338 }
339 Cryptography::Providers::OpenSSL::Exception::ThrowLastErrorIfFailed (::EVP_CipherInit_ex (
340 ctx, nullptr, NULL, reinterpret_cast<unsigned char*> (useKey.begin ()), reinterpret_cast<unsigned char*> (useIV.begin ()), enc));
341 }
342}
343
344OpenSSLCryptoParams::OpenSSLCryptoParams (CipherAlgorithm alg, const BLOB& key, const BLOB& initialIV)
345{
346 using namespace Providers::OpenSSL;
347 bool nopad = false;
348 bool useArgumentKeyLength = false;
349 if (alg == CipherAlgorithms::kRC2_CBC () or alg == CipherAlgorithms::kRC2_ECB () or alg == CipherAlgorithms::kRC2_CFB () or
350 alg == CipherAlgorithms::kRC2_OFB () or alg == CipherAlgorithms::kRC4 ()) {
351 useArgumentKeyLength = true;
352 }
353 fInitializer = [=] (::EVP_CIPHER_CTX* ctx, Direction d) {
354 ApplySettings2CTX_ (ctx, alg, d, nopad, useArgumentKeyLength, key, initialIV);
355 };
356}
357
358OpenSSLCryptoParams::OpenSSLCryptoParams (CipherAlgorithm alg, const DerivedKey& derivedKey)
359 : OpenSSLCryptoParams{alg, derivedKey.fKey, derivedKey.fIV}
360{
361}
362
363/*
364 ********************************************************************************
365 ******************** Cryptography::OpenSSLInputStream **************************
366 ********************************************************************************
367 */
368auto OpenSSLInputStream::New (const OpenSSLCryptoParams& cryptoParams, Direction direction, const InputStream::Ptr<byte>& realIn)
370{
371 return Streams::InputStream::Ptr<byte>{make_shared<OpenSSLInputStreamRep_> (cryptoParams, direction, realIn)};
372}
373
374auto OpenSSLInputStream::New (Execution::InternallySynchronized internallySynchronized, const OpenSSLCryptoParams& cryptoParams,
375 Direction direction, const InputStream::Ptr<byte>& realIn) -> Streams::InputStream::Ptr<byte>
376{
377 switch (internallySynchronized) {
378 case Execution::eInternallySynchronized:
379 return Streams::InternallySynchronizedInputStream::New<OpenSSLInputStreamRep_> ({}, cryptoParams, direction, realIn);
380 case Execution::eNotKnownInternallySynchronized:
381 return New (cryptoParams, direction, realIn);
382 default:
384 return New (cryptoParams, direction, realIn);
385 }
386}
387
388/*
389 ********************************************************************************
390 ******************* Cryptography::OpenSSLOutputStream **************************
391 ********************************************************************************
392 */
393auto OpenSSLOutputStream::New (const OpenSSLCryptoParams& cryptoParams, Direction direction, const OutputStream::Ptr<byte>& realOut)
395{
396 return Streams::OutputStream::Ptr<byte>{make_shared<OpenSSLOutputStreamRep_> (cryptoParams, direction, realOut)};
397}
398
399auto OpenSSLOutputStream::New (Execution::InternallySynchronized internallySynchronized, const OpenSSLCryptoParams& cryptoParams,
400 Direction direction, const OutputStream::Ptr<byte>& realOut) -> Streams::OutputStream::Ptr<byte>
401{
402 switch (internallySynchronized) {
403 case Execution::eInternallySynchronized:
404 return Streams::InternallySynchronizedOutputStream::New<OpenSSLOutputStreamRep_> ({}, cryptoParams, direction, realOut);
405 case Execution::eNotKnownInternallySynchronized:
406 return New (cryptoParams, direction, realOut);
407 default:
409 return New (cryptoParams, direction, realOut);
410 }
411}
412#endif
#define RequireNotReached()
Definition Assertions.h:385
#define RequireNotNull(p)
Definition Assertions.h:347
#define Verify(c)
Definition Assertions.h:419
NoDataAvailableHandling
If eDontBlock passed to most Stream APIs, then when the code would do a blocking read,...
Definition Stream.h:90
nonvirtual size_t length() const
Definition BLOB.inl:271
nonvirtual bool empty() const
Definition BLOB.inl:246
nonvirtual const byte * begin() const
Definition BLOB.inl:253
nonvirtual size_t size() const
Definition BLOB.inl:281
Logically halfway between std::array and std::vector; Smart 'direct memory array' - which when needed...
virtual bool IsSeekable() const =0
virtual SeekOffsetType GetReadOffset() const =0
virtual optional< span< ElementType > > Read(span< ElementType > intoBuffer, NoDataAvailableHandling blockFlag)=0
virtual optional< SeekOffsetType > RemainingLength()
returns nullopt if not known (typical, and the default) - but sometimes it is known,...
virtual optional< size_t > AvailableToRead()
returns nullopt if nothing known available, zero if known EOF, and any other number of elements (typi...
InputStream<>::Ptr is Smart pointer (with abstract Rep) class defining the interface to reading from ...
Abstract interface for output stream object. Don't call directly (use Ptr usually) - but use directly...
virtual void Write(span< const ELEMENT_TYPE > elts)=0
OutputStream<>::Ptr is Smart pointer to a stream-based sink of data.