Files
2025-12-10 14:38:26 -08:00

872 lines
42 KiB
C++

// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************
#include "open_ssl.h"
#include <bit> //std::endian (needed for macOS)
#include "base64.h"
#include "thread.h"
#include "argon2.h"
#include "serialize.h"
#include <openssl/pem.h>
#include <openssl/err.h>
#include <openssl/ssl.h>
#include <openssl/core_names.h>
#include <openssl/encoder.h>
#include <openssl/decoder.h>
#include <openssl/param_build.h>
using namespace zen;
namespace
{
#ifndef OPENSSL_THREADS
#error FFS, we are royally screwed!
#endif
/* Sign a file using SHA-256:
openssl dgst -sha256 -sign private.pem -out file.sig file.txt
verify the signature: (caveat: public key expected to be in pkix format!)
openssl dgst -sha256 -verify public.pem -signature file.sig file.txt */
std::wstring formatOpenSSLError(const char* functionName, unsigned long ec)
{
char errorBuf[256] = {}; //== buffer size used by ERR_error_string(); err.c: it seems the message uses at most ~200 bytes
::ERR_error_string_n(ec, errorBuf, sizeof(errorBuf)); //includes null-termination
return formatSystemError(functionName, replaceCpy(_("Error code %x"), L"%x", numberTo<std::wstring>(ec)), utfTo<std::wstring>(errorBuf));
}
std::wstring formatLastOpenSSLError(const char* functionName)
{
const auto ec = ::ERR_peek_last_error(); //"returns latest error code from the thread's error queue without modifying it" - unlike ERR_get_error()
//ERR_get_error: "returns the earliest error code from the thread's error queue and removes the entry.
// This function can be called repeatedly until there are no more error codes to return."
::ERR_clear_error(); //clean up for next OpenSSL operation on this thread
return formatOpenSSLError(functionName, ec);
}
}
void zen::openSslInit()
{
//official Wiki: https://wiki.openssl.org/index.php/Library_Initialization
//see apps_shutdown(): https://github.com/openssl/openssl/blob/master/apps/openssl.c
//see Curl_ossl_cleanup(): https://github.com/curl/curl/blob/master/lib/vtls/openssl.c
assert(runningOnMainThread());
//explicitly init OpenSSL on main thread: seems to initialize atomically! But it still might help to avoid issues:
//https://www.openssl.org/docs/manmaster/man3/OPENSSL_init_ssl.html
if (::OPENSSL_init_ssl(OPENSSL_INIT_SSL_DEFAULT | OPENSSL_INIT_NO_LOAD_CONFIG, nullptr) != 1)
logExtraError(_("Error during process initialization.") + L"\n\n" + formatLastOpenSSLError("OPENSSL_init_ssl"));
}
void zen::openSslTearDown() {}
//OpenSSL 1.1.0+ deprecates all clean up functions
//=> so much the theory, in practice it leaks, of course: https://github.com/openssl/openssl/issues/6283
namespace
{
struct OpenSslThreadCleanUp
{
~OpenSslThreadCleanUp()
{
::OPENSSL_thread_stop();
}
};
thread_local OpenSslThreadCleanUp tearDownOpenSslThreadData;
//================================================================================
std::shared_ptr<EVP_PKEY> generateRsaKeyPair(int bits) //throw SysError
{
EVP_PKEY_CTX* keyCtx = ::EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, //int id
nullptr); //ENGINE* e
if (!keyCtx)
throw SysError(formatLastOpenSSLError("EVP_PKEY_CTX_new_id"));
ZEN_ON_SCOPE_EXIT(::EVP_PKEY_CTX_free(keyCtx));
if (::EVP_PKEY_keygen_init(keyCtx) != 1)
throw SysError(formatLastOpenSSLError("EVP_PKEY_keygen_init"));
//"RSA keys set the key length during key generation rather than parameter generation"
if (::EVP_PKEY_CTX_set_rsa_keygen_bits(keyCtx, bits) <= 0) //"[...] return a positive value for success" => effectively returns "1"
throw SysError(formatLastOpenSSLError("EVP_PKEY_CTX_set_rsa_keygen_bits"));
EVP_PKEY* keyPair = nullptr;
if (::EVP_PKEY_keygen(keyCtx, &keyPair) != 1)
throw SysError(formatLastOpenSSLError("EVP_PKEY_keygen"));
return std::shared_ptr<EVP_PKEY>(keyPair, ::EVP_PKEY_free);
}
//================================================================================
std::shared_ptr<EVP_PKEY> streamToKey(const std::string_view keyStream, RsaStreamType streamType, bool publicKey) //throw SysError
{
switch (streamType)
{
case RsaStreamType::pkix:
{
BIO* bio = ::BIO_new_mem_buf(keyStream.data(), static_cast<int>(keyStream.size()));
if (!bio)
throw SysError(formatLastOpenSSLError("BIO_new_mem_buf"));
ZEN_ON_SCOPE_EXIT(::BIO_free_all(bio));
if (EVP_PKEY* evp = (publicKey ?
::PEM_read_bio_PUBKEY :
::PEM_read_bio_PrivateKey)
(bio, //BIO* bp
nullptr, //EVP_PKEY** x
nullptr, //pem_password_cb* cb
nullptr)) //void* u
return std::shared_ptr<EVP_PKEY>(evp, ::EVP_PKEY_free);
throw SysError(formatLastOpenSSLError(publicKey ? "PEM_read_bio_PUBKEY" : "PEM_read_bio_PrivateKey"));
}
case RsaStreamType::pkcs1:
{
EVP_PKEY* evp = nullptr;
auto guardEvp = makeGuard<ScopeGuardRunMode::onExit>([&] { if (evp) ::EVP_PKEY_free(evp); });
const int selection = publicKey ? OSSL_KEYMGMT_SELECT_PUBLIC_KEY : OSSL_KEYMGMT_SELECT_PRIVATE_KEY;
OSSL_DECODER_CTX* decCtx = ::OSSL_DECODER_CTX_new_for_pkey(&evp, //EVP_PKEY** pkey
"PEM", //const char* input_type
nullptr, //const char* input_struct
"RSA", //const char* keytype
selection, //int selection
nullptr, //OSSL_LIB_CTX* libctx
nullptr); //const char* propquery
if (!decCtx)
throw SysError(formatLastOpenSSLError("OSSL_DECODER_CTX_new_for_pkey"));
ZEN_ON_SCOPE_EXIT(::OSSL_DECODER_CTX_free(decCtx));
#if 0 //key stream is password-protected? => OSSL_DECODER_CTX_set_passphrase()
if (!password.empty())
if (::OSSL_DECODER_CTX_set_passphrase(decCtx, //OSSL_DECODER_CTX *ctx
reinterpret_cast<const unsigned char*>(password.c_str()), //const unsigned char* kstr
password.size()) != 1) //size_t klen
throw SysError(formatLastOpenSSLError("OSSL_DECODER_CTX_set_passphrase"));
#endif
const unsigned char* keyBuf = reinterpret_cast<const unsigned char*>(keyStream.data());
size_t keyLen = keyStream.size();
if (::OSSL_DECODER_from_data(decCtx, &keyBuf, &keyLen) != 1)
throw SysError(formatLastOpenSSLError("OSSL_DECODER_from_data"));
guardEvp.dismiss(); //pass ownership
return std::shared_ptr<EVP_PKEY>(evp, ::EVP_PKEY_free); //
}
case RsaStreamType::raw:
break;
}
auto tmp = reinterpret_cast<const unsigned char*>(keyStream.data());
EVP_PKEY* evp = (publicKey ? ::d2i_PublicKey : ::d2i_PrivateKey)(EVP_PKEY_RSA, //int type
nullptr, //EVP_PKEY** a
&tmp, /*changes tmp pointer itself!*/ //const unsigned char** pp
static_cast<long>(keyStream.size())); //long length
if (!evp)
throw SysError(formatLastOpenSSLError(publicKey ? "d2i_PublicKey" : "d2i_PrivateKey"));
return std::shared_ptr<EVP_PKEY>(evp, ::EVP_PKEY_free);
}
//================================================================================
std::string keyToStream(const EVP_PKEY* evp, RsaStreamType streamType, bool publicKey) //throw SysError
{
//assert(::EVP_PKEY_get_base_id(evp) == EVP_PKEY_RSA);
switch (streamType)
{
case RsaStreamType::pkix:
{
//fix OpenSSL API inconsistencies:
auto PEM_write_bio_PrivateKey2 = [](BIO* bio, const EVP_PKEY* key)
{
return ::PEM_write_bio_PrivateKey(bio, //BIO* bp
key, //const EVP_PKEY* x
nullptr, //const EVP_CIPHER* enc
nullptr, //const unsigned char* kstr
0, //int klen
nullptr, //pem_password_cb* cb
nullptr); //void* u
};
BIO* bio = ::BIO_new(BIO_s_mem());
if (!bio)
throw SysError(formatLastOpenSSLError("BIO_new"));
ZEN_ON_SCOPE_EXIT(::BIO_free_all(bio));
if ((publicKey ?
::PEM_write_bio_PUBKEY :
PEM_write_bio_PrivateKey2)(bio, evp) != 1)
throw SysError(formatLastOpenSSLError(publicKey ? "PEM_write_bio_PUBKEY" : "PEM_write_bio_PrivateKey"));
//---------------------------------------------
const int keyLen = BIO_pending(bio);
if (keyLen < 0)
throw SysError(formatLastOpenSSLError("BIO_pending"));
if (keyLen == 0)
throw SysError(formatSystemError("BIO_pending", L"", L"No more error details.")); //no more error details
std::string keyStream(keyLen, '\0');
if (::BIO_read(bio, keyStream.data(), keyLen) != keyLen)
throw SysError(formatLastOpenSSLError("BIO_read"));
return keyStream;
}
case RsaStreamType::pkcs1:
{
const int selection = publicKey ? OSSL_KEYMGMT_SELECT_PUBLIC_KEY : OSSL_KEYMGMT_SELECT_PRIVATE_KEY;
OSSL_ENCODER_CTX* encCtx = ::OSSL_ENCODER_CTX_new_for_pkey(evp, //const EVP_PKEY* pkey
selection, //int selection
"PEM", //const char* output_type
nullptr, //const char* output_structure
nullptr); //const char* propquery
if (!encCtx)
throw SysError(formatLastOpenSSLError("OSSL_ENCODER_CTX_new_for_pkey"));
ZEN_ON_SCOPE_EXIT(::OSSL_ENCODER_CTX_free(encCtx));
//password-protect stream? => OSSL_ENCODER_CTX_set_passphrase()
unsigned char* keyBuf = nullptr;
size_t keyLen = 0;
if (::OSSL_ENCODER_to_data(encCtx, &keyBuf, &keyLen) != 1)
throw SysError(formatLastOpenSSLError("OSSL_ENCODER_to_data"));
ZEN_ON_SCOPE_EXIT(::OPENSSL_free(keyBuf));
return {reinterpret_cast<const char*>(keyBuf), keyLen};
}
case RsaStreamType::raw:
break;
}
unsigned char* buf = nullptr;
const int bufSize = (publicKey ? ::i2d_PublicKey : ::i2d_PrivateKey)(evp, &buf);
if (bufSize <= 0)
throw SysError(formatLastOpenSSLError(publicKey ? "i2d_PublicKey" : "i2d_PrivateKey"));
ZEN_ON_SCOPE_EXIT(::OPENSSL_free(buf)); //memory is only allocated for bufSize > 0
return {reinterpret_cast<const char*>(buf), static_cast<size_t>(bufSize)};
}
//================================================================================
std::string createHash(const std::string_view str, const EVP_MD* type) //throw SysError
{
std::string output(EVP_MAX_MD_SIZE, '\0');
unsigned int bytesWritten = 0;
#if 1
//https://www.openssl.org/docs/manmaster/man3/EVP_Digest.html
if (::EVP_Digest(str.data(), //const void* data
str.size(), //size_t count
reinterpret_cast<unsigned char*>(output.data()), //unsigned char* md
&bytesWritten, //unsigned int* size
type, //const EVP_MD* type
nullptr) != 1) //ENGINE* impl
throw SysError(formatLastOpenSSLError("EVP_Digest"));
#else //streaming version
EVP_MD_CTX* mdctx = ::EVP_MD_CTX_new();
if (!mdctx)
throw SysError(formatSystemError("EVP_MD_CTX_new", L"", L"No more error details.")); //no more error details
ZEN_ON_SCOPE_EXIT(::EVP_MD_CTX_free(mdctx));
if (::EVP_DigestInit(mdctx, //EVP_MD_CTX* ctx
type) != 1) //const EVP_MD* type
throw SysError(formatLastOpenSSLError("EVP_DigestInit"));
if (::EVP_DigestUpdate(mdctx, //EVP_MD_CTX* ctx
str.data(), //const void*
str.size()) != 1) //size_t cnt
throw SysError(formatLastOpenSSLError("EVP_DigestUpdate"));
if (::EVP_DigestFinal_ex(mdctx, //EVP_MD_CTX* ctx
reinterpret_cast<unsigned char*>(output.data()), //unsigned char* md
&bytesWritten) != 1) //unsigned int* s
throw SysError(formatLastOpenSSLError("EVP_DigestFinal_ex"));
#endif
output.resize(bytesWritten);
return output;
}
std::string createSignature(const std::string_view message, EVP_PKEY* privateKey) //throw SysError
{
//https://www.openssl.org/docs/manmaster/man3/EVP_DigestSign.html
EVP_MD_CTX* mdctx = ::EVP_MD_CTX_new();
if (!mdctx)
throw SysError(formatSystemError("EVP_MD_CTX_new", L"", L"No more error details.")); //no more error details
ZEN_ON_SCOPE_EXIT(::EVP_MD_CTX_free(mdctx));
if (::EVP_DigestSignInit(mdctx, //EVP_MD_CTX* ctx
nullptr, //EVP_PKEY_CTX** pctx
EVP_sha256(), //const EVP_MD* type
nullptr, //ENGINE* e
privateKey) != 1) //EVP_PKEY* pkey
throw SysError(formatLastOpenSSLError("EVP_DigestSignInit"));
if (::EVP_DigestSignUpdate(mdctx, //EVP_MD_CTX* ctx
message.data(), //const void* d
message.size()) != 1) //size_t cnt
throw SysError(formatLastOpenSSLError("EVP_DigestSignUpdate"));
size_t sigLenMax = 0; //"first call to EVP_DigestSignFinal returns the maximum buffer size required"
if (::EVP_DigestSignFinal(mdctx, //EVP_MD_CTX* ctx
nullptr, //unsigned char* sigret
&sigLenMax) != 1) //size_t* siglen
throw SysError(formatLastOpenSSLError("EVP_DigestSignFinal"));
std::string signature(sigLenMax, '\0');
size_t sigLen = sigLenMax;
if (::EVP_DigestSignFinal(mdctx, //EVP_MD_CTX* ctx
reinterpret_cast<unsigned char*>(signature.data()), //unsigned char* sigret
&sigLen) != 1) //size_t* siglen
throw SysError(formatLastOpenSSLError("EVP_DigestSignFinal"));
signature.resize(sigLen);
return signature;
}
void verifySignature(const std::string_view message, const std::string_view signature, EVP_PKEY* publicKey) //throw SysError
{
//https://www.openssl.org/docs/manmaster/man3/EVP_DigestVerify.html
EVP_MD_CTX* mdctx = ::EVP_MD_CTX_new();
if (!mdctx)
throw SysError(formatSystemError("EVP_MD_CTX_new", L"", L"No more error details.")); //no more error details
ZEN_ON_SCOPE_EXIT(::EVP_MD_CTX_free(mdctx));
if (::EVP_DigestVerifyInit(mdctx, //EVP_MD_CTX* ctx
nullptr, //EVP_PKEY_CTX** pctx
EVP_sha256(), //const EVP_MD* type
nullptr, //ENGINE* e
publicKey) != 1) //EVP_PKEY* pkey
throw SysError(formatLastOpenSSLError("EVP_DigestVerifyInit"));
if (::EVP_DigestVerifyUpdate(mdctx, //EVP_MD_CTX* ctx
message.data(), //const void* d
message.size()) != 1) //size_t cnt
throw SysError(formatLastOpenSSLError("EVP_DigestVerifyUpdate"));
if (::EVP_DigestVerifyFinal(mdctx, //EVP_MD_CTX* ctx
reinterpret_cast<const unsigned char*>(signature.data()), //const unsigned char* sig
signature.size()) != 1) //size_t siglen
throw SysError(formatLastOpenSSLError("EVP_DigestVerifyFinal"));
}
}
std::string zen::convertRsaKey(const std::string_view keyStream, RsaStreamType typeFrom, RsaStreamType typeTo, bool publicKey) //throw SysError
{
assert(typeFrom != typeTo);
std::shared_ptr<EVP_PKEY> evp = streamToKey(keyStream, typeFrom, publicKey); //throw SysError
return keyToStream(evp.get(), typeTo, publicKey); //throw SysError
}
void zen::verifySignature(const std::string_view message, const std::string_view signature, const std::string_view publicKeyStream, RsaStreamType streamType) //throw SysError
{
std::shared_ptr<EVP_PKEY> publicKey = streamToKey(publicKeyStream, streamType, true /*publicKey*/); //throw SysError
::verifySignature(message, signature, publicKey.get()); //throw SysError
}
bool zen::isPuttyKeyStream(const std::string_view keyStream)
{
return startsWith(trimCpy(keyStream, TrimSide::left), "PuTTY-User-Key-File-");
}
std::string zen::convertPuttyKeyToPkix(const std::string_view keyStream, const std::string_view passphrase) //throw SysError
{
std::vector<std::string_view> lines;
split2(keyStream, isLineBreak<char>, [&lines](const std::string_view block)
{
if (!block.empty()) //consider Windows' <CR><LF>
lines.push_back(block);
});
//----------- parse PuTTY ppk structure ----------------------------------
auto itLine = lines.begin();
auto lineStartsWith = [&](const char* str)
{
return itLine != lines.end() && startsWith(*itLine, str);
};
const int ppkFormat = [&]
{
if (lineStartsWith("PuTTY-User-Key-File-2: "))
return 2;
else if (lineStartsWith("PuTTY-User-Key-File-3: "))
return 3;
else
throw SysError(L"Unknown key file format");
}();
const std::string_view algorithm = afterFirst(*itLine++, ' ', IfNotFoundReturn::none);
if (!lineStartsWith("Encryption: "))
throw SysError(L"Missing key encryption");
const std::string_view keyEncryption = afterFirst(*itLine++, ' ', IfNotFoundReturn::none);
const bool keyEncrypted = keyEncryption == "aes256-cbc";
if (!keyEncrypted && keyEncryption != "none")
throw SysError(L"Unknown key encryption");
if (!lineStartsWith("Comment: "))
throw SysError(L"Missing comment");
const std::string_view comment = afterFirst(*itLine++, ' ', IfNotFoundReturn::none);
if (!lineStartsWith("Public-Lines: "))
throw SysError(L"Missing public lines");
size_t pubLineCount = stringTo<size_t>(afterFirst(*itLine++, ' ', IfNotFoundReturn::none));
std::string publicBlob64;
while (pubLineCount-- != 0)
if (itLine != lines.end())
publicBlob64 += *itLine++;
else
throw SysError(L"Invalid key: incomplete public lines");
Argon2Flavor argonFlavor = Argon2Flavor::d;
uint32_t argonMemory = 0;
uint32_t argonPasses = 0;
uint32_t argonParallelism = 0;
std::string argonSalt;
if (ppkFormat >= 3 && keyEncrypted)
{
if (!lineStartsWith("Key-Derivation: "))
throw SysError(L"Missing Argon2 parameter: Key-Derivation");
const std::string_view keyDerivation = afterFirst(*itLine++, ' ', IfNotFoundReturn::none);
argonFlavor = [&]
{
if (keyDerivation == "Argon2d")
return Argon2Flavor::d;
else if (keyDerivation == "Argon2i")
return Argon2Flavor::i;
else if (keyDerivation == "Argon2id")
return Argon2Flavor::id;
else
throw SysError(L"Unexpected Argon2 parameter for Key-Derivation");
}();
if (!lineStartsWith("Argon2-Memory: "))
throw SysError(L"Missing Argon2 parameter: Argon2-Memory");
argonMemory = stringTo<uint32_t>(afterFirst(*itLine++, ' ', IfNotFoundReturn::none));
if (!lineStartsWith("Argon2-Passes: "))
throw SysError(L"Missing Argon2 parameter: Argon2-Passes");
argonPasses = stringTo<uint32_t>(afterFirst(*itLine++, ' ', IfNotFoundReturn::none));
if (!lineStartsWith("Argon2-Parallelism: "))
throw SysError(L"Missing Argon2 parameter: Argon2-Parallelism");
argonParallelism = stringTo<uint32_t>(afterFirst(*itLine++, ' ', IfNotFoundReturn::none));
if (!lineStartsWith("Argon2-Salt: "))
throw SysError(L"Missing Argon2 parameter: Argon2-Salt");
const std::string_view argonSaltHex = afterFirst(*itLine++, ' ', IfNotFoundReturn::none);
if (argonSaltHex.size() % 2 != 0 || !std::all_of(argonSaltHex.begin(), argonSaltHex.end(), isHexDigit<char>))
throw SysError(L"Invalid Argon2 parameter: Argon2-Salt");
for (size_t i = 0; i < argonSaltHex.size(); i += 2)
argonSalt += unhexify(argonSaltHex[i], argonSaltHex[i + 1]);
}
if (!lineStartsWith("Private-Lines: "))
throw SysError(L"Missing private lines");
size_t privLineCount = stringTo<size_t>(afterFirst(*itLine++, ' ', IfNotFoundReturn::none));
std::string privateBlob64;
while (privLineCount-- != 0)
if (itLine != lines.end())
privateBlob64 += *itLine++;
else
throw SysError(L"Invalid key: incomplete private lines");
if (!lineStartsWith("Private-MAC: "))
throw SysError(L"MAC missing"); //apparently "Private-Hash" is/was possible here: maybe with ppk version 1!?
const std::string_view macHex = afterFirst(*itLine++, ' ', IfNotFoundReturn::none);
//----------- unpack key file elements ---------------------
if (macHex.size() % 2 != 0 || !std::all_of(macHex.begin(), macHex.end(), isHexDigit<char>))
throw SysError(L"Invalid key: invalid MAC");
std::string mac;
for (size_t i = 0; i < macHex.size(); i += 2)
mac += unhexify(macHex[i], macHex[i + 1]);
const std::string publicBlob = stringDecodeBase64(publicBlob64);
const std::string privateBlobEnc = stringDecodeBase64(privateBlob64);
std::string privateBlob;
std::string macKeyFmt3;
if (!keyEncrypted)
privateBlob = privateBlobEnc;
else
{
if (passphrase.empty())
throw SysError(L"Passphrase required to access private key");
const EVP_CIPHER* const cipher = EVP_aes_256_cbc();
std::string decryptKey;
std::string iv;
if (ppkFormat >= 3)
{
decryptKey.resize(::EVP_CIPHER_get_key_length(cipher));
iv .resize(::EVP_CIPHER_get_iv_length (cipher));
macKeyFmt3.resize(32);
const std::string argonBlob = zargon2(argonFlavor, argonMemory, argonPasses, argonParallelism,
static_cast<uint32_t>(decryptKey.size() + iv.size() + macKeyFmt3.size()), passphrase, argonSalt);
MemoryStreamIn streamIn(argonBlob);
readArray(streamIn, decryptKey.data(), decryptKey.size()); //
readArray(streamIn, iv .data(), iv .size()); //throw SysErrorUnexpectedEos
readArray(streamIn, macKeyFmt3.data(), macKeyFmt3.size()); //
}
else
{
decryptKey = createHash(std::string("\0\0\0\0", 4) + passphrase, EVP_sha1()) + //throw SysError
createHash(std::string("\0\0\0\1", 4) + passphrase, EVP_sha1()); //
decryptKey.resize(::EVP_CIPHER_get_key_length(cipher)); //PuTTYgen only uses first 32 bytes as key (== key length of EVP_aes_256_cbc)
iv.assign(::EVP_CIPHER_get_iv_length(cipher), 0); //initialization vector is 16-byte-range of zeros (== default for EVP_aes_256_cbc)
}
EVP_CIPHER_CTX* cipCtx = ::EVP_CIPHER_CTX_new();
if (!cipCtx)
throw SysError(formatSystemError("EVP_CIPHER_CTX_new", L"", L"No more error details.")); //no more error details
ZEN_ON_SCOPE_EXIT(::EVP_CIPHER_CTX_free(cipCtx));
if (::EVP_DecryptInit(cipCtx, //EVP_CIPHER_CTX* ctx
cipher, //const EVP_CIPHER* type
reinterpret_cast<const unsigned char*>(decryptKey.c_str()), //const unsigned char* key
reinterpret_cast<const unsigned char*>(iv.c_str())) != 1) //const unsigned char* iv => nullptr = 16-byte zeros for EVP_aes_256_cbc
throw SysError(formatLastOpenSSLError("EVP_DecryptInit_ex"));
if (::EVP_CIPHER_CTX_set_padding(cipCtx, 0 /*padding*/) != 1)
throw SysError(formatSystemError("EVP_CIPHER_CTX_set_padding", L"", L"No more error details.")); //no more error details
privateBlob.resize(privateBlobEnc.size() + ::EVP_CIPHER_block_size(EVP_aes_256_cbc()));
//"EVP_DecryptUpdate() should have room for (inl + cipher_block_size) bytes"
int decLen1 = 0;
if (::EVP_DecryptUpdate(cipCtx, //EVP_CIPHER_CTX* ctx
reinterpret_cast<unsigned char*>(privateBlob.data()), //unsigned char* out
&decLen1, //int* outl
reinterpret_cast<const unsigned char*>(privateBlobEnc.c_str()), //const unsigned char* in
static_cast<int>(privateBlobEnc.size())) != 1) //int inl
throw SysError(formatLastOpenSSLError("EVP_DecryptUpdate"));
int decLen2 = 0;
if (::EVP_DecryptFinal(cipCtx, //EVP_CIPHER_CTX* ctx
reinterpret_cast<unsigned char*>(&privateBlob[decLen1]), //unsigned char* outm
&decLen2) != 1) //int* outl
throw SysError(formatLastOpenSSLError("EVP_DecryptFinal_ex"));
privateBlob.resize(decLen1 + decLen2);
}
//----------- verify key consistency ---------------------
std::string macKey;
if (ppkFormat >= 3)
macKey = macKeyFmt3;
else
macKey = createHash(std::string("putty-private-key-file-mac-key") + (keyEncrypted ? passphrase : ""), EVP_sha1()); //throw SysError
auto numToBeString = [](size_t n) -> std::string
{
static_assert(std::endian::native == std::endian::little && sizeof(n) >= 4);
assert(n == static_cast<uint32_t>(n));
const char* numStr = reinterpret_cast<const char*>(&n);
return {numStr[3], numStr[2], numStr[1], numStr[0]}; //big endian!
};
const std::string macData = numToBeString(algorithm .size()) + algorithm +
numToBeString(keyEncryption.size()) + keyEncryption +
numToBeString(comment .size()) + comment +
numToBeString(publicBlob .size()) + publicBlob +
numToBeString(privateBlob .size()) + privateBlob;
char md[EVP_MAX_MD_SIZE] = {};
unsigned int mdLen = 0;
if (!::HMAC(ppkFormat <= 2 ? EVP_sha1() : EVP_sha256(), //const EVP_MD* evp_md
macKey.c_str(), //const void* key
static_cast<int>(macKey.size()), //int key_len
reinterpret_cast<const unsigned char*>(macData.c_str()), //const unsigned char* d
static_cast<int>(macData.size()), //int n
reinterpret_cast<unsigned char*>(md), //unsigned char* md
&mdLen)) //unsigned int* md_len
throw SysError(formatSystemError("HMAC", L"", L"No more error details.")); //no more error details
if (mac != std::string_view(md, mdLen))
throw SysError(keyEncrypted ? L"Wrong passphrase (or corrupted key)" : L"Validation failed: corrupted key");
//----------------------------------------------------------
auto extractString = [](auto& it, auto itEnd)
{
uint32_t byteCount = 0;
if (itEnd - it < makeSigned(sizeof(byteCount)))
throw SysError(L"String extraction failed: unexpected end of stream");
static_assert(std::endian::native == std::endian::little);
char* numStr = reinterpret_cast<char*>(&byteCount);
numStr[3] = *it++; //
numStr[2] = *it++; //Putty uses big endian!
numStr[1] = *it++; //
numStr[0] = *it++; //
if (makeUnsigned(itEnd - it) < byteCount)
throw SysError(L"String extraction failed: unexpected end of stream(2)");
std::string str(it, it + byteCount);
it += byteCount;
return str;
};
struct BnFree { void operator()(BIGNUM* num) const { ::BN_free(num); } };
auto createBigNum = []
{
BIGNUM* bn = ::BN_new();
if (!bn)
throw SysError(formatLastOpenSSLError("BN_new"));
return std::unique_ptr<BIGNUM, BnFree>(bn);
};
auto extractBigNum = [&extractString](auto& it, auto itEnd)
{
const std::string bytes = extractString(it, itEnd);
BIGNUM* bn = ::BN_bin2bn(reinterpret_cast<const unsigned char*>(bytes.c_str()), static_cast<int>(bytes.size()), nullptr);
if (!bn)
throw SysError(formatLastOpenSSLError("BN_bin2bn"));
return std::unique_ptr<BIGNUM, BnFree>(bn);
};
auto itPub = publicBlob .begin();
auto itPriv = privateBlob.begin();
auto extractStringPub = [&] { return extractString(itPub, publicBlob .end()); };
auto extractStringPriv = [&] { return extractString(itPriv, privateBlob.end()); };
auto extractBigNumPub = [&] { return extractBigNum(itPub, publicBlob .end()); };
auto extractBigNumPriv = [&] { return extractBigNum(itPriv, privateBlob.end()); };
//----------- parse public/private key blobs ----------------
if (extractStringPub() != algorithm)
throw SysError(L"Invalid public key stream (header)");
if (algorithm == "ssh-rsa")
{
std::unique_ptr<BIGNUM, BnFree> e = extractBigNumPub (); //
std::unique_ptr<BIGNUM, BnFree> n = extractBigNumPub (); //
std::unique_ptr<BIGNUM, BnFree> d = extractBigNumPriv(); //throw SysError
std::unique_ptr<BIGNUM, BnFree> p = extractBigNumPriv(); //
std::unique_ptr<BIGNUM, BnFree> q = extractBigNumPriv(); //
std::unique_ptr<BIGNUM, BnFree> iqmp = extractBigNumPriv(); //
//------ calculate missing numbers: dmp1, dmq1 -------------
std::unique_ptr<BIGNUM, BnFree> dmp1 = createBigNum(); //
std::unique_ptr<BIGNUM, BnFree> dmq1 = createBigNum(); //throw SysError
std::unique_ptr<BIGNUM, BnFree> tmp = createBigNum(); //
BN_CTX* bnCtx = BN_CTX_new();
if (!bnCtx)
throw SysError(formatLastOpenSSLError("BN_CTX_new"));
ZEN_ON_SCOPE_EXIT(::BN_CTX_free(bnCtx));
if (::BN_sub(tmp.get(), p.get(), BN_value_one()) != 1)
throw SysError(formatLastOpenSSLError("BN_sub"));
if (::BN_mod(dmp1.get(), d.get(), tmp.get(), bnCtx) != 1)
throw SysError(formatLastOpenSSLError("BN_mod"));
if (::BN_sub(tmp.get(), q.get(), BN_value_one()) != 1)
throw SysError(formatLastOpenSSLError("BN_sub"));
if (::BN_mod(dmq1.get(), d.get(), tmp.get(), bnCtx) != 1)
throw SysError(formatLastOpenSSLError("BN_mod"));
//----------------------------------------------------------
OSSL_PARAM_BLD* paramBld = ::OSSL_PARAM_BLD_new();
if (!paramBld)
throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_new"));
ZEN_ON_SCOPE_EXIT(::OSSL_PARAM_BLD_free(paramBld));
if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_RSA_N, n.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(n)"));
if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_RSA_E, e.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(e)"));
if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_RSA_D, d.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(d)"));
if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_RSA_FACTOR1, p.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(p)"));
if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_RSA_FACTOR2, q.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(q)"));
if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_RSA_EXPONENT1, dmp1.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(dmp1)"));
if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_RSA_EXPONENT2, dmq1.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(dmq1)"));
if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_RSA_COEFFICIENT1, iqmp.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(iqmp)"));
OSSL_PARAM* sslParams = ::OSSL_PARAM_BLD_to_param(paramBld);
if (!sslParams)
throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_to_param"));
ZEN_ON_SCOPE_EXIT(::OSSL_PARAM_free(sslParams));
EVP_PKEY_CTX* evpCtx = ::EVP_PKEY_CTX_new_from_name(nullptr, "RSA", nullptr);
if (!evpCtx)
throw SysError(formatLastOpenSSLError("EVP_PKEY_CTX_new_from_name(RSA)"));
ZEN_ON_SCOPE_EXIT(::EVP_PKEY_CTX_free(evpCtx));
if (::EVP_PKEY_fromdata_init(evpCtx) != 1)
throw SysError(formatLastOpenSSLError("EVP_PKEY_fromdata_init"));
EVP_PKEY* evp = nullptr;
if (::EVP_PKEY_fromdata(evpCtx, &evp, EVP_PKEY_KEYPAIR, sslParams) != 1)
throw SysError(formatLastOpenSSLError("EVP_PKEY_fromdata"));
ZEN_ON_SCOPE_EXIT(::EVP_PKEY_free(evp));
return keyToStream(evp, RsaStreamType::pkix, false /*publicKey*/); //throw SysError
}
//----------------------------------------------------------
else if (algorithm == "ssh-dss")
{
std::unique_ptr<BIGNUM, BnFree> p = extractBigNumPub (); //
std::unique_ptr<BIGNUM, BnFree> q = extractBigNumPub (); //
std::unique_ptr<BIGNUM, BnFree> g = extractBigNumPub (); //throw SysError
std::unique_ptr<BIGNUM, BnFree> pub = extractBigNumPub (); //
std::unique_ptr<BIGNUM, BnFree> pri = extractBigNumPriv(); //
//----------------------------------------------------------
OSSL_PARAM_BLD* paramBld = ::OSSL_PARAM_BLD_new();
if (!paramBld)
throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_new"));
ZEN_ON_SCOPE_EXIT(::OSSL_PARAM_BLD_free(paramBld));
if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_FFC_P, p.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(p)"));
if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_FFC_Q, q.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(q)"));
if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_FFC_G, g.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(g)"));
if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_PUB_KEY, pub.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(pub)"));
if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_PRIV_KEY, pri.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(pri)"));
OSSL_PARAM* sslParams = ::OSSL_PARAM_BLD_to_param(paramBld);
if (!sslParams)
throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_to_param"));
ZEN_ON_SCOPE_EXIT(::OSSL_PARAM_free(sslParams));
EVP_PKEY_CTX* evpCtx = ::EVP_PKEY_CTX_new_from_name(nullptr, "DSA", nullptr);
if (!evpCtx)
throw SysError(formatLastOpenSSLError("EVP_PKEY_CTX_new_from_name(DSA)"));
ZEN_ON_SCOPE_EXIT(::EVP_PKEY_CTX_free(evpCtx));
if (::EVP_PKEY_fromdata_init(evpCtx) != 1)
throw SysError(formatLastOpenSSLError("EVP_PKEY_fromdata_init"));
EVP_PKEY* evp = nullptr;
if (::EVP_PKEY_fromdata(evpCtx, &evp, EVP_PKEY_KEYPAIR, sslParams) != 1)
throw SysError(formatLastOpenSSLError("EVP_PKEY_fromdata"));
ZEN_ON_SCOPE_EXIT(::EVP_PKEY_free(evp));
return keyToStream(evp, RsaStreamType::pkix, false /*publicKey*/); //throw SysError
}
//----------------------------------------------------------
else if (algorithm == "ecdsa-sha2-nistp256" ||
algorithm == "ecdsa-sha2-nistp384" ||
algorithm == "ecdsa-sha2-nistp521")
{
const std::string_view algoShort = afterLast(algorithm, '-', IfNotFoundReturn::none);
if (extractStringPub() != algoShort)
throw SysError(L"Invalid public key stream (header)");
const std::string pointStream = extractStringPub();
std::unique_ptr<BIGNUM, BnFree> pri = extractBigNumPriv(); //throw SysError
//----------------------------------------------------------
const char* groupName = [&]
{
if (algoShort == "nistp256")
return SN_X9_62_prime256v1; //same as SECG secp256r1
if (algoShort == "nistp384")
return SN_secp384r1;
if (algoShort == "nistp521")
return SN_secp521r1;
throw SysError(L"Unknown elliptic curve: " + utfTo<std::wstring>(algorithm));
}();
OSSL_PARAM_BLD* paramBld = ::OSSL_PARAM_BLD_new();
if (!paramBld)
throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_new"));
ZEN_ON_SCOPE_EXIT(::OSSL_PARAM_BLD_free(paramBld));
if (::OSSL_PARAM_BLD_push_utf8_string(paramBld, OSSL_PKEY_PARAM_GROUP_NAME, groupName, 0) != 1)
throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_utf8_string(group)"));
if (::OSSL_PARAM_BLD_push_octet_string(paramBld, OSSL_PKEY_PARAM_PUB_KEY, pointStream.data(), pointStream.size()) != 1)
throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_octet_string(pub)"));
if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_PRIV_KEY, pri.get()) != 1)
throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(priv)"));
OSSL_PARAM* sslParams = ::OSSL_PARAM_BLD_to_param(paramBld);
if (!sslParams)
throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_to_param"));
ZEN_ON_SCOPE_EXIT(::OSSL_PARAM_free(sslParams));
EVP_PKEY_CTX* evpCtx = ::EVP_PKEY_CTX_new_from_name(nullptr, "EC", nullptr);
if (!evpCtx)
throw SysError(formatLastOpenSSLError("EVP_PKEY_CTX_new_from_name(EC)"));
ZEN_ON_SCOPE_EXIT(::EVP_PKEY_CTX_free(evpCtx));
if (::EVP_PKEY_fromdata_init(evpCtx) != 1)
throw SysError(formatLastOpenSSLError("EVP_PKEY_fromdata_init"));
EVP_PKEY* evp = nullptr;
if (::EVP_PKEY_fromdata(evpCtx, &evp, EVP_PKEY_KEYPAIR, sslParams) != 1)
throw SysError(formatLastOpenSSLError("EVP_PKEY_fromdata"));
ZEN_ON_SCOPE_EXIT(::EVP_PKEY_free(evp));
return keyToStream(evp, RsaStreamType::pkix, false /*publicKey*/); //throw SysError
}
//----------------------------------------------------------
else if (algorithm == "ssh-ed25519")
{
//const std::string pubStream = extractStringPub(); -> we don't need the public key
const std::string priStream = extractStringPriv();
EVP_PKEY* evpPriv = ::EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, //int type
nullptr, //ENGINE* e
reinterpret_cast<const unsigned char*>(priStream.c_str()), //const unsigned char* priv
priStream.size()); //size_t len
if (!evpPriv)
throw SysError(formatLastOpenSSLError("EVP_PKEY_new_raw_private_key"));
ZEN_ON_SCOPE_EXIT(::EVP_PKEY_free(evpPriv));
return keyToStream(evpPriv, RsaStreamType::pkix, false /*publicKey*/); //throw SysError
}
else
throw SysError(L"Unsupported key algorithm: " + utfTo<std::wstring>(algorithm));
/* PuTTYgen supports many more (which are not yet supported by libssh2):
- rsa-sha2-256
- rsa-sha2-512
- ssh-ed448
- ssh-dss-cert-v01@openssh.com
- ssh-rsa-cert-v01@openssh.com
- rsa-sha2-256-cert-v01@openssh.com
- rsa-sha2-512-cert-v01@openssh.com
- ssh-ed25519-cert-v01@openssh.com
- ecdsa-sha2-nistp256-cert-v01@openssh.com
- ecdsa-sha2-nistp384-cert-v01@openssh.com
- ecdsa-sha2-nistp521-cert-v01@openssh.com */
}