Compare commits
3 Commits
e6ed0e6d2f
...
dev_openss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d49482ea4c | ||
|
|
fe4affd942 | ||
|
|
21088468b7 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,4 +8,5 @@
|
||||
/toolchain/sysroot/
|
||||
/toolchain/build/
|
||||
|
||||
.vscode/
|
||||
.vscode/
|
||||
check/
|
||||
@@ -37,6 +37,7 @@ set( HEADERS
|
||||
src/Services/DeviceControlService.h
|
||||
src/Services/EnrollmentService.h
|
||||
src/Security/TlsKeyUtil.h
|
||||
src/Security/SslCertUtil.h
|
||||
)
|
||||
|
||||
add_executable( ${PROJECT_NAME} ${SOURCES} ${HEADERS} )
|
||||
|
||||
512
src/Security/SslCertUtil.h
Normal file
512
src/Security/SslCertUtil.h
Normal file
@@ -0,0 +1,512 @@
|
||||
// src/Security/SslCertUtil.h
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <sys/stat.h> // chmod
|
||||
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/ec.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/x509v3.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/hmac.h>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
namespace snoop
|
||||
{
|
||||
namespace device_sec
|
||||
{
|
||||
|
||||
class SslCertUtil
|
||||
{
|
||||
public:
|
||||
// Generates:
|
||||
// - EC P-256 private key at keyPath (PEM, chmod 600)
|
||||
// - CSR at csrPath with:
|
||||
// CN = guid
|
||||
// subjectAltName = URI:urn:device:<guid>
|
||||
static void GenerateEcKeyAndCsr(const std::string &guid,
|
||||
const std::filesystem::path &keyPath,
|
||||
const std::filesystem::path &csrPath)
|
||||
{
|
||||
spdlog::info("SslCertUtil: generating EC key + CSR for GUID={}", guid);
|
||||
|
||||
EVP_PKEY *pkey = nullptr;
|
||||
EC_KEY *ec = nullptr;
|
||||
|
||||
try
|
||||
{
|
||||
// ----- 1) Generate EC key (prime256v1) -----
|
||||
ec = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
|
||||
if (!ec)
|
||||
{
|
||||
throw std::runtime_error("EC_KEY_new_by_curve_name failed");
|
||||
}
|
||||
if (EC_KEY_generate_key(ec) != 1)
|
||||
{
|
||||
throw std::runtime_error("EC_KEY_generate_key failed");
|
||||
}
|
||||
|
||||
pkey = EVP_PKEY_new();
|
||||
if (!pkey)
|
||||
{
|
||||
throw std::runtime_error("EVP_PKEY_new failed");
|
||||
}
|
||||
if (EVP_PKEY_assign_EC_KEY(pkey, ec) != 1)
|
||||
{
|
||||
throw std::runtime_error("EVP_PKEY_assign_EC_KEY failed");
|
||||
}
|
||||
// pkey now owns ec
|
||||
ec = nullptr; // prevent double free
|
||||
|
||||
// ----- 5) Write key + CSR to disk -----
|
||||
SavePrivateKeyPem(pkey, keyPath);
|
||||
BuildAndSaveCsr(pkey, guid, csrPath);
|
||||
|
||||
spdlog::info("SslCertUtil: key written to '{}', csr written to '{}'",
|
||||
keyPath.string(), csrPath.string());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
if (pkey)
|
||||
{
|
||||
EVP_PKEY_free(pkey);
|
||||
}
|
||||
if (ec)
|
||||
{
|
||||
EC_KEY_free(ec);
|
||||
}
|
||||
throw; // rethrow
|
||||
}
|
||||
|
||||
EVP_PKEY_free(pkey);
|
||||
}
|
||||
|
||||
// CSR from an existing key
|
||||
static void GenerateCsrFromExistingKey(const std::string &guid,
|
||||
const std::filesystem::path &keyPath,
|
||||
const std::filesystem::path &csrPath)
|
||||
{
|
||||
spdlog::info("SslCertUtil: generating CSR from existing key '{}'", keyPath.string());
|
||||
|
||||
FILE *kf = fopen(keyPath.string().c_str(), "rb");
|
||||
if (!kf)
|
||||
{
|
||||
throw std::runtime_error("GenerateCsrFromExistingKey: cannot open key file: " +
|
||||
keyPath.string());
|
||||
}
|
||||
|
||||
EVP_PKEY *pkey = PEM_read_PrivateKey(kf, nullptr, nullptr, nullptr);
|
||||
fclose(kf);
|
||||
|
||||
if (!pkey)
|
||||
{
|
||||
throw std::runtime_error("GenerateCsrFromExistingKey: PEM_read_PrivateKey failed");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
BuildAndSaveCsr(pkey, guid, csrPath);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
EVP_PKEY_free(pkey);
|
||||
throw;
|
||||
}
|
||||
|
||||
EVP_PKEY_free(pkey);
|
||||
spdlog::info("SslCertUtil: CSR written to '{}'", csrPath.string());
|
||||
}
|
||||
|
||||
// Encrypt file with AES-256-CBC + PBKDF2 + salt.
|
||||
//
|
||||
// Equivalent to:
|
||||
// openssl enc -aes-256-cbc -pbkdf2 -salt -pass pass:<password> -in in -out out
|
||||
//
|
||||
// NOTE: Does NOT shred or delete the input file; caller should do that
|
||||
// (you’ll still call `shred -u` from EnrollmentService).
|
||||
static void EncryptFileAes256CbcPbkdf2(const std::filesystem::path &inPath,
|
||||
const std::filesystem::path &outPath,
|
||||
const std::string &password)
|
||||
{
|
||||
spdlog::info("SslCertUtil: encrypting '{}' -> '{}' (AES-256-CBC + PBKDF2)",
|
||||
inPath.string(), outPath.string());
|
||||
|
||||
// ----- 1) Read input file -----
|
||||
std::ifstream fin(inPath, std::ios::binary);
|
||||
if (!fin)
|
||||
{
|
||||
throw std::runtime_error("EncryptFileAes256CbcPbkdf2: cannot open input file: " +
|
||||
inPath.string());
|
||||
}
|
||||
std::vector<unsigned char> plaintext(
|
||||
(std::istreambuf_iterator<char>(fin)),
|
||||
std::istreambuf_iterator<char>());
|
||||
fin.close();
|
||||
|
||||
// ----- 2) Prepare salt, key, iv -----
|
||||
unsigned char salt[8];
|
||||
if (RAND_bytes(salt, sizeof(salt)) != 1)
|
||||
{
|
||||
throw std::runtime_error("EncryptFileAes256CbcPbkdf2: RAND_bytes failed");
|
||||
}
|
||||
|
||||
const EVP_CIPHER *cipher = EVP_aes_256_cbc();
|
||||
const int keyLen = EVP_CIPHER_key_length(cipher);
|
||||
const int ivLen = EVP_CIPHER_iv_length(cipher);
|
||||
|
||||
std::vector<unsigned char> keyiv(keyLen + ivLen);
|
||||
|
||||
const int iterations = 10000; // PBKDF2 rounds
|
||||
if (PKCS5_PBKDF2_HMAC(password.c_str(),
|
||||
static_cast<int>(password.size()),
|
||||
salt, sizeof(salt),
|
||||
iterations,
|
||||
EVP_sha256(),
|
||||
keyLen + ivLen,
|
||||
keyiv.data()) != 1)
|
||||
{
|
||||
throw std::runtime_error("EncryptFileAes256CbcPbkdf2: PKCS5_PBKDF2_HMAC failed");
|
||||
}
|
||||
|
||||
unsigned char *key = keyiv.data();
|
||||
unsigned char *iv = keyiv.data() + keyLen;
|
||||
|
||||
// ----- 3) Encrypt -----
|
||||
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
|
||||
if (!ctx)
|
||||
{
|
||||
throw std::runtime_error("EncryptFileAes256CbcPbkdf2: EVP_CIPHER_CTX_new failed");
|
||||
}
|
||||
|
||||
std::vector<unsigned char> ciphertext(plaintext.size() + EVP_CIPHER_block_size(cipher));
|
||||
int outLen1 = 0;
|
||||
int outLen2 = 0;
|
||||
|
||||
if (EVP_EncryptInit_ex(ctx, cipher, nullptr, key, iv) != 1)
|
||||
{
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
throw std::runtime_error("EncryptFileAes256CbcPbkdf2: EVP_EncryptInit_ex failed");
|
||||
}
|
||||
|
||||
if (EVP_EncryptUpdate(ctx,
|
||||
ciphertext.data(), &outLen1,
|
||||
plaintext.data(), static_cast<int>(plaintext.size())) != 1)
|
||||
{
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
throw std::runtime_error("EncryptFileAes256CbcPbkdf2: EVP_EncryptUpdate failed");
|
||||
}
|
||||
|
||||
if (EVP_EncryptFinal_ex(ctx,
|
||||
ciphertext.data() + outLen1, &outLen2) != 1)
|
||||
{
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
throw std::runtime_error("EncryptFileAes256CbcPbkdf2: EVP_EncryptFinal_ex failed");
|
||||
}
|
||||
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
ciphertext.resize(outLen1 + outLen2);
|
||||
|
||||
// ----- 4) Write output: [salt][ciphertext] -----
|
||||
std::ofstream fout(outPath, std::ios::binary | std::ios::trunc);
|
||||
if (!fout)
|
||||
{
|
||||
throw std::runtime_error("EncryptFileAes256CbcPbkdf2: cannot open output file: " +
|
||||
outPath.string());
|
||||
}
|
||||
|
||||
// Just raw salt followed by ciphertext
|
||||
fout.write(reinterpret_cast<const char *>(salt), sizeof(salt));
|
||||
fout.write(reinterpret_cast<const char *>(ciphertext.data()),
|
||||
static_cast<std::streamsize>(ciphertext.size()));
|
||||
fout.close();
|
||||
|
||||
spdlog::info("SslCertUtil: encryption finished, wrote {}", outPath.string());
|
||||
}
|
||||
|
||||
// Compute HMAC-SHA256 and return lowercase hex string.
|
||||
// Equivalent to:
|
||||
// printf %s "<data>" | openssl dgst -sha256 -hmac "<key>" | awk '{print $2}'
|
||||
static std::string ComputeHmacSha256Hex(const std::string &key,
|
||||
const std::string &data)
|
||||
{
|
||||
unsigned char mac[EVP_MAX_MD_SIZE];
|
||||
unsigned int macLen = 0;
|
||||
|
||||
if (!HMAC(EVP_sha256(),
|
||||
key.data(), static_cast<int>(key.size()),
|
||||
reinterpret_cast<const unsigned char *>(data.data()),
|
||||
data.size(),
|
||||
mac, &macLen))
|
||||
{
|
||||
throw std::runtime_error("ComputeHmacSha256Hex: HMAC(EVP_sha256) failed");
|
||||
}
|
||||
|
||||
return ToHexLower(mac, macLen);
|
||||
}
|
||||
|
||||
// Decrypt file with AES-256-CBC + PBKDF2 + salt.
|
||||
// Layout: [8-byte salt][ciphertext]
|
||||
// Password is KEK (hex string from HMAC).
|
||||
static void DecryptFileAes256CbcPbkdf2(const std::filesystem::path &inPath,
|
||||
const std::filesystem::path &outPath,
|
||||
const std::string &password)
|
||||
{
|
||||
spdlog::info("SslCertUtil: decrypting '{}' -> '{}' (AES-256-CBC + PBKDF2)",
|
||||
inPath.string(), outPath.string());
|
||||
|
||||
// ----- 1) Read input file -----
|
||||
std::ifstream fin(inPath, std::ios::binary);
|
||||
if (!fin)
|
||||
{
|
||||
throw std::runtime_error("DecryptFileAes256CbcPbkdf2: cannot open input file: " +
|
||||
inPath.string());
|
||||
}
|
||||
std::vector<unsigned char> enc(
|
||||
(std::istreambuf_iterator<char>(fin)),
|
||||
std::istreambuf_iterator<char>());
|
||||
fin.close();
|
||||
|
||||
if (enc.size() < 8)
|
||||
{
|
||||
throw std::runtime_error("DecryptFileAes256CbcPbkdf2: encrypted file too small");
|
||||
}
|
||||
|
||||
// First 8 bytes = salt, rest = ciphertext
|
||||
unsigned char salt[8];
|
||||
std::copy(enc.begin(), enc.begin() + 8, salt);
|
||||
std::vector<unsigned char> ciphertext(enc.begin() + 8, enc.end());
|
||||
|
||||
// ----- 2) Derive key/iv -----
|
||||
const EVP_CIPHER *cipher = EVP_aes_256_cbc();
|
||||
const int keyLen = EVP_CIPHER_key_length(cipher);
|
||||
const int ivLen = EVP_CIPHER_iv_length(cipher);
|
||||
|
||||
std::vector<unsigned char> keyiv(keyLen + ivLen);
|
||||
|
||||
const int iterations = 10000;
|
||||
if (PKCS5_PBKDF2_HMAC(password.c_str(),
|
||||
static_cast<int>(password.size()),
|
||||
salt, sizeof(salt),
|
||||
iterations,
|
||||
EVP_sha256(),
|
||||
keyLen + ivLen,
|
||||
keyiv.data()) != 1)
|
||||
{
|
||||
throw std::runtime_error("DecryptFileAes256CbcPbkdf2: PKCS5_PBKDF2_HMAC failed");
|
||||
}
|
||||
|
||||
unsigned char *key = keyiv.data();
|
||||
unsigned char *iv = keyiv.data() + keyLen;
|
||||
|
||||
// ----- 3) Decrypt -----
|
||||
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
|
||||
if (!ctx)
|
||||
{
|
||||
throw std::runtime_error("DecryptFileAes256CbcPbkdf2: EVP_CIPHER_CTX_new failed");
|
||||
}
|
||||
|
||||
std::vector<unsigned char> plaintext(ciphertext.size() + EVP_CIPHER_block_size(cipher));
|
||||
int outLen1 = 0;
|
||||
int outLen2 = 0;
|
||||
|
||||
if (EVP_DecryptInit_ex(ctx, cipher, nullptr, key, iv) != 1)
|
||||
{
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
throw std::runtime_error("DecryptFileAes256CbcPbkdf2: EVP_DecryptInit_ex failed");
|
||||
}
|
||||
|
||||
if (EVP_DecryptUpdate(ctx,
|
||||
plaintext.data(), &outLen1,
|
||||
ciphertext.data(), static_cast<int>(ciphertext.size())) != 1)
|
||||
{
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
throw std::runtime_error("DecryptFileAes256CbcPbkdf2: EVP_DecryptUpdate failed");
|
||||
}
|
||||
|
||||
if (EVP_DecryptFinal_ex(ctx,
|
||||
plaintext.data() + outLen1, &outLen2) != 1)
|
||||
{
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
throw std::runtime_error("DecryptFileAes256CbcPbkdf2: EVP_DecryptFinal_ex failed");
|
||||
}
|
||||
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
plaintext.resize(outLen1 + outLen2);
|
||||
|
||||
// ----- 4) Write plaintext -----
|
||||
std::filesystem::create_directories(outPath.parent_path());
|
||||
std::ofstream fout(outPath, std::ios::binary | std::ios::trunc);
|
||||
if (!fout)
|
||||
{
|
||||
throw std::runtime_error("DecryptFileAes256CbcPbkdf2: cannot open output file: " +
|
||||
outPath.string());
|
||||
}
|
||||
|
||||
fout.write(reinterpret_cast<const char *>(plaintext.data()),
|
||||
static_cast<std::streamsize>(plaintext.size()));
|
||||
fout.close();
|
||||
|
||||
spdlog::info("SslCertUtil: decryption finished, wrote {}", outPath.string());
|
||||
}
|
||||
|
||||
private:
|
||||
static void BuildAndSaveCsr(EVP_PKEY *pkey,
|
||||
const std::string &guid,
|
||||
const std::filesystem::path &csrPath)
|
||||
{
|
||||
X509_REQ *req = X509_REQ_new();
|
||||
if (!req)
|
||||
{
|
||||
throw std::runtime_error("BuildAndSaveCsr: X509_REQ_new failed");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Subject: CN = GUID
|
||||
X509_NAME *name = X509_NAME_new();
|
||||
if (!name)
|
||||
{
|
||||
throw std::runtime_error("BuildAndSaveCsr: X509_NAME_new failed");
|
||||
}
|
||||
if (X509_NAME_add_entry_by_NID(
|
||||
name,
|
||||
NID_commonName,
|
||||
MBSTRING_ASC,
|
||||
reinterpret_cast<const unsigned char *>(guid.c_str()),
|
||||
-1, -1, 0) != 1)
|
||||
{
|
||||
X509_NAME_free(name);
|
||||
throw std::runtime_error("BuildAndSaveCsr: X509_NAME_add_entry_by_NID(CN) failed");
|
||||
}
|
||||
if (X509_REQ_set_subject_name(req, name) != 1)
|
||||
{
|
||||
X509_NAME_free(name);
|
||||
throw std::runtime_error("BuildAndSaveCsr: X509_REQ_set_subject_name failed");
|
||||
}
|
||||
X509_NAME_free(name);
|
||||
|
||||
// Public key
|
||||
if (X509_REQ_set_pubkey(req, pkey) != 1)
|
||||
{
|
||||
throw std::runtime_error("BuildAndSaveCsr: X509_REQ_set_pubkey failed");
|
||||
}
|
||||
|
||||
// subjectAltName = URI:urn:device:<GUID>
|
||||
X509V3_CTX ctx;
|
||||
X509V3_set_ctx_nodb(&ctx);
|
||||
X509V3_set_ctx(&ctx, nullptr, nullptr, req, nullptr, 0);
|
||||
|
||||
std::string sanStr = "URI:urn:device:" + guid;
|
||||
X509_EXTENSION *ext = X509V3_EXT_conf_nid(
|
||||
nullptr, &ctx, NID_subject_alt_name,
|
||||
const_cast<char *>(sanStr.c_str()));
|
||||
if (!ext)
|
||||
{
|
||||
throw std::runtime_error("BuildAndSaveCsr: X509V3_EXT_conf_nid(subjectAltName) failed");
|
||||
}
|
||||
|
||||
STACK_OF(X509_EXTENSION) *exts = sk_X509_EXTENSION_new_null();
|
||||
if (!exts)
|
||||
{
|
||||
X509_EXTENSION_free(ext);
|
||||
throw std::runtime_error("BuildAndSaveCsr: sk_X509_EXTENSION_new_null failed");
|
||||
}
|
||||
sk_X509_EXTENSION_push(exts, ext);
|
||||
if (X509_REQ_add_extensions(req, exts) != 1)
|
||||
{
|
||||
sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free);
|
||||
throw std::runtime_error("BuildAndSaveCsr: X509_REQ_add_extensions failed");
|
||||
}
|
||||
sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free);
|
||||
|
||||
// Sign with SHA-256
|
||||
if (X509_REQ_sign(req, pkey, EVP_sha256()) <= 0)
|
||||
{
|
||||
throw std::runtime_error("BuildAndSaveCsr: X509_REQ_sign failed");
|
||||
}
|
||||
|
||||
SaveCsrPem(req, csrPath);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
X509_REQ_free(req);
|
||||
throw;
|
||||
}
|
||||
|
||||
X509_REQ_free(req);
|
||||
}
|
||||
|
||||
static void SavePrivateKeyPem(EVP_PKEY *pkey, const std::filesystem::path &keyPath)
|
||||
{
|
||||
const auto parent = keyPath.parent_path();
|
||||
if (!parent.empty())
|
||||
{
|
||||
std::filesystem::create_directories(parent);
|
||||
}
|
||||
|
||||
FILE *f = fopen(keyPath.string().c_str(), "wb");
|
||||
if (!f)
|
||||
{
|
||||
throw std::runtime_error("SavePrivateKeyPem: fopen failed: " + keyPath.string());
|
||||
}
|
||||
|
||||
if (PEM_write_PrivateKey(f, pkey, nullptr, nullptr, 0, nullptr, nullptr) != 1)
|
||||
{
|
||||
fclose(f);
|
||||
throw std::runtime_error("SavePrivateKeyPem: PEM_write_PrivateKey failed");
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
|
||||
// chmod 600
|
||||
::chmod(keyPath.string().c_str(), 0600);
|
||||
}
|
||||
|
||||
static void SaveCsrPem(X509_REQ *req, const std::filesystem::path &csrPath)
|
||||
{
|
||||
const auto parent = csrPath.parent_path();
|
||||
if (!parent.empty())
|
||||
{
|
||||
std::filesystem::create_directories(parent);
|
||||
}
|
||||
|
||||
FILE *f = fopen(csrPath.string().c_str(), "wb");
|
||||
if (!f)
|
||||
{
|
||||
throw std::runtime_error("SaveCsrPem: fopen failed: " + csrPath.string());
|
||||
}
|
||||
|
||||
if (PEM_write_X509_REQ(f, req) != 1)
|
||||
{
|
||||
fclose(f);
|
||||
throw std::runtime_error("SaveCsrPem: PEM_write_X509_REQ failed");
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static std::string ToHexLower(const unsigned char *buf, size_t len)
|
||||
{
|
||||
static const char *hex = "0123456789abcdef";
|
||||
std::string out;
|
||||
out.reserve(len * 2);
|
||||
|
||||
for (size_t i = 0; i < len; ++i)
|
||||
{
|
||||
unsigned char b = buf[i];
|
||||
out.push_back(hex[b >> 4]);
|
||||
out.push_back(hex[b & 0x0F]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace device_sec
|
||||
} // namespace snoop
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include "SslCertUtil.h"
|
||||
|
||||
namespace snoop
|
||||
{
|
||||
@@ -20,7 +21,9 @@ namespace snoop
|
||||
auto b = s.find_first_not_of(" \t\r\n");
|
||||
auto e = s.find_last_not_of(" \t\r\n");
|
||||
if (b == std::string::npos)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return s.substr(b, e - b + 1);
|
||||
}
|
||||
|
||||
@@ -30,13 +33,19 @@ namespace snoop
|
||||
std::string out;
|
||||
FILE *pipe = popen((cmd + " 2>&1").c_str(), "r");
|
||||
if (!pipe)
|
||||
{
|
||||
throw std::runtime_error("popen failed: " + cmd);
|
||||
}
|
||||
while (fgets(buf.data(), (int)buf.size(), pipe) != nullptr)
|
||||
{
|
||||
out.append(buf.data());
|
||||
}
|
||||
int rc = pclose(pipe);
|
||||
int exitCode = WIFEXITED(rc) ? WEXITSTATUS(rc) : rc;
|
||||
if (exitCode != 0)
|
||||
{
|
||||
spdlog::warn("Command '{}' exited with code {}", cmd, exitCode);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -45,12 +54,16 @@ namespace snoop
|
||||
{
|
||||
std::string id = Trim(Exec("keyctl search @s user iot-client-key | tail -n1"));
|
||||
if (id.empty())
|
||||
{
|
||||
throw std::runtime_error("iot-client-key not found in keyring");
|
||||
}
|
||||
// Create a secure temp file
|
||||
char tmpl[] = "/run/iot/iot-keyXXXXXX";
|
||||
int fd = mkstemp(tmpl);
|
||||
if (fd < 0)
|
||||
{
|
||||
throw std::runtime_error("mkstemp failed for client key");
|
||||
}
|
||||
close(fd);
|
||||
std::filesystem::path p(tmpl);
|
||||
|
||||
@@ -79,7 +92,9 @@ namespace snoop
|
||||
name.push_back('\0');
|
||||
fd = mkstemp(name.data());
|
||||
if (fd < 0)
|
||||
{
|
||||
throw std::runtime_error("mkstemp failed");
|
||||
}
|
||||
fchmod(fd, S_IRUSR | S_IWUSR);
|
||||
path = name.data();
|
||||
}
|
||||
@@ -91,7 +106,9 @@ namespace snoop
|
||||
{
|
||||
ssize_t w = ::write(fd, p + off, n - off);
|
||||
if (w <= 0)
|
||||
{
|
||||
throw std::runtime_error("write failed");
|
||||
}
|
||||
off += (size_t)w;
|
||||
}
|
||||
fsync(fd);
|
||||
@@ -99,7 +116,9 @@ namespace snoop
|
||||
~TempFile()
|
||||
{
|
||||
if (fd >= 0)
|
||||
{
|
||||
::close(fd);
|
||||
}
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(path, ec);
|
||||
}
|
||||
@@ -110,14 +129,113 @@ namespace snoop
|
||||
// 1) get key id
|
||||
std::string id = Trim(Exec("keyctl search @s user iot-client-key | tail -n1"));
|
||||
if (id.empty())
|
||||
{
|
||||
throw std::runtime_error("iot-client-key not found in keyring");
|
||||
|
||||
}
|
||||
|
||||
// 2) capture payload (no redirection to file)
|
||||
std::string bytes = Exec("keyctl pipe " + id);
|
||||
if (bytes.empty())
|
||||
{
|
||||
throw std::runtime_error("keyctl pipe returned empty payload");
|
||||
}
|
||||
|
||||
return std::vector<uint8_t>(bytes.begin(), bytes.end());
|
||||
}
|
||||
|
||||
static std::string GetCpuSerial()
|
||||
{
|
||||
std::ifstream f("/proc/cpuinfo");
|
||||
if (!f)
|
||||
{
|
||||
spdlog::warn("GetCpuSerial: /proc/cpuinfo not available, using fallback");
|
||||
return std::string(12, '9');
|
||||
}
|
||||
|
||||
std::string line;
|
||||
std::regex re(R"(^\s*Serial\s*:\s*([0-9A-Fa-f]+)\s*$)");
|
||||
while (std::getline(f, line))
|
||||
{
|
||||
std::smatch m;
|
||||
if (std::regex_match(line, m, re) && m.size() == 2)
|
||||
{
|
||||
auto serial = Trim(m[1].str());
|
||||
if (!serial.empty())
|
||||
{
|
||||
return serial;
|
||||
}
|
||||
}
|
||||
}
|
||||
spdlog::warn("GetCpuSerial: Serial not found, using fallback");
|
||||
return std::string(12, '9');
|
||||
}
|
||||
|
||||
inline bool IsClientKeyInKernelKeyring()
|
||||
{
|
||||
std::string id = Trim(Exec("keyctl search @s user iot-client-key | tail -n1"));
|
||||
return !id.empty();
|
||||
}
|
||||
|
||||
// Ensure that iot-client-key exists in kernel keyring.
|
||||
// - If already present: no-op, returns false (nothing loaded).
|
||||
// - If not present: decrypts /etc/iot/keys/device.key.enc using KEK
|
||||
// derived from (GUID, CPU_SERIAL), padds into keyring, returns true.
|
||||
inline bool EnsureClientKeyInKernelKeyring(const std::string &guid)
|
||||
{
|
||||
if (IsClientKeyInKernelKeyring())
|
||||
{
|
||||
spdlog::info("EnsureClientKeyInKernelKeyring: key already present, nothing to do");
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::filesystem::path encPath = "/etc/iot/keys/device.key.enc";
|
||||
if (!std::filesystem::exists(encPath))
|
||||
{
|
||||
throw std::runtime_error("EnsureClientKeyInKernelKeyring: encrypted key not found at " +
|
||||
encPath.string());
|
||||
}
|
||||
|
||||
// Derive KEK = HMAC-SHA256(cpuSerial, key=GUID)
|
||||
const std::string cpuSerial = GetCpuSerial();
|
||||
spdlog::info("EnsureClientKeyInKernelKeyring: CPU_SERIAL = {}", cpuSerial);
|
||||
|
||||
const std::string kek =
|
||||
snoop::device_sec::SslCertUtil::ComputeHmacSha256Hex(guid, cpuSerial);
|
||||
|
||||
if (kek.empty())
|
||||
{
|
||||
throw std::runtime_error("EnsureClientKeyInKernelKeyring: empty KEK");
|
||||
}
|
||||
spdlog::debug("EnsureClientKeyInKernelKeyring: KEK (hex) = {}", kek);
|
||||
|
||||
// Decrypt into tmpfs: /run/iot/device.key
|
||||
const std::filesystem::path runDir = "/run/iot";
|
||||
const std::filesystem::path plainPath = runDir / "device.key";
|
||||
std::filesystem::create_directories(runDir);
|
||||
|
||||
snoop::device_sec::SslCertUtil::DecryptFileAes256CbcPbkdf2(
|
||||
encPath,
|
||||
plainPath,
|
||||
kek);
|
||||
|
||||
if (!std::filesystem::exists(plainPath))
|
||||
{
|
||||
throw std::runtime_error("EnsureClientKeyInKernelKeyring: decrypted key not created");
|
||||
}
|
||||
|
||||
// Load into kernel keyring
|
||||
const std::string cmd = "keyctl padd user iot-client-key @s < " + plainPath.string();
|
||||
spdlog::info("EnsureClientKeyInKernelKeyring: loading key into kernel keyring...");
|
||||
auto out = Exec(cmd);
|
||||
spdlog::debug("keyctl padd output:\n{}", out);
|
||||
|
||||
// Securely erase plaintext
|
||||
const std::string shredCmd = "shred -u " + plainPath.string();
|
||||
spdlog::info("EnsureClientKeyInKernelKeyring: shredding plaintext key with '{}'", shredCmd);
|
||||
auto shredOut = Exec(shredCmd);
|
||||
spdlog::debug("shred output:\n{}", shredOut);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,11 +75,11 @@ namespace snoop
|
||||
if (this->m_writingThread.joinable())
|
||||
{
|
||||
this->m_writingThread.join();
|
||||
}
|
||||
}
|
||||
if (this->m_uploadThread.joinable())
|
||||
{
|
||||
this->m_uploadThread.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------- Public control API (called from DeviceControlService handlers) --------
|
||||
@@ -135,9 +135,11 @@ namespace snoop
|
||||
spdlog::info("StopRecordingNow ignored: not recording");
|
||||
return;
|
||||
}
|
||||
auto stoppedAtMs = NowMs();
|
||||
|
||||
// Force-close current segment right away, enqueue, and disable recording.
|
||||
this->m_oggWriter->StopWriting();
|
||||
this->MoveToUploadQueue(this->m_currentRecordFilePath);
|
||||
this->MoveToUploadQueue(this->m_currentRecordFilePath,this->m_currentRecordStartedAt,stoppedAtMs);
|
||||
m_recordingEnabled = false;
|
||||
m_stopAfterCurrentSegment = false;
|
||||
spdlog::info("Recording stopped immediately (deep sleep)");
|
||||
@@ -207,7 +209,9 @@ namespace snoop
|
||||
// Setup CA/CRT (from enrollment)
|
||||
std::filesystem::path ca = "/etc/iot/keys/issuing_ca.pem";
|
||||
if (!std::filesystem::exists(ca))
|
||||
{
|
||||
ca = "/etc/iot/keys/ca_chain.pem";
|
||||
}
|
||||
const std::filesystem::path crt = "/etc/iot/keys/device.crt.pem";
|
||||
|
||||
if (!needReinit)
|
||||
@@ -262,7 +266,8 @@ namespace snoop
|
||||
|
||||
void WritingThread()
|
||||
{
|
||||
// recording starts ONLY when StartRecording() is called
|
||||
constexpr unsigned long long DEFAULT_SEGMENT_MS = 30'000; // 30 seconds
|
||||
|
||||
while (!m_isIntermission)
|
||||
{
|
||||
if (!m_recordingEnabled.load())
|
||||
@@ -271,32 +276,52 @@ namespace snoop
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start a fresh segment
|
||||
auto now = std::chrono::system_clock::now();
|
||||
// --- Get segment duration from config ---
|
||||
unsigned long long segDurationMs = this->m_configService->GetRecordingDuration();
|
||||
if (segDurationMs < 1000) // anything < 1s is basically useless here
|
||||
{
|
||||
spdlog::warn("RecordingDuration={} ms is too small, using default {} ms",
|
||||
segDurationMs, DEFAULT_SEGMENT_MS);
|
||||
segDurationMs = DEFAULT_SEGMENT_MS;
|
||||
}
|
||||
spdlog::info("Starting new segment with target duration={} ms", segDurationMs);
|
||||
|
||||
// --- Mark start times (wall + monotonic) ---
|
||||
auto wallStart = std::chrono::system_clock::now();
|
||||
this->m_currentRecordStartedAt =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
|
||||
this->m_currentRecordFilePath = this->m_destinationDirectoryPath + std::to_string(this->m_currentRecordStartedAt);
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(wallStart.time_since_epoch()).count();
|
||||
|
||||
this->m_currentRecordFilePath =
|
||||
this->m_destinationDirectoryPath + std::to_string(this->m_currentRecordStartedAt);
|
||||
|
||||
this->m_oggWriter->StartWriting(this->m_currentRecordFilePath);
|
||||
spdlog::info("Recording segment started: {}", this->m_currentRecordFilePath);
|
||||
|
||||
// Write until duration elapses
|
||||
const auto segDurationMs = this->m_configService->GetRecordingDuration();
|
||||
auto monoStart = std::chrono::steady_clock::now();
|
||||
const auto targetDuration = std::chrono::milliseconds(segDurationMs);
|
||||
|
||||
// --- Wait until segment duration elapses ---
|
||||
while (!m_isIntermission && m_recordingEnabled.load())
|
||||
{
|
||||
now = std::chrono::system_clock::now();
|
||||
auto currentRecordDuration =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count() - this->m_currentRecordStartedAt;
|
||||
if (currentRecordDuration >= segDurationMs)
|
||||
{
|
||||
auto elapsed = std::chrono::steady_clock::now() - monoStart;
|
||||
if (elapsed >= targetDuration)
|
||||
break;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
}
|
||||
|
||||
// Close current segment and enqueue
|
||||
auto wallStop = std::chrono::system_clock::now();
|
||||
auto stoppedAtMs =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(wallStop.time_since_epoch()).count();
|
||||
|
||||
// --- Close and enqueue ---
|
||||
this->m_oggWriter->StopWriting();
|
||||
this->MoveToUploadQueue(this->m_currentRecordFilePath);
|
||||
spdlog::info("Recording segment finished: {}", this->m_currentRecordFilePath);
|
||||
this->MoveToUploadQueue(this->m_currentRecordFilePath,
|
||||
this->m_currentRecordStartedAt,
|
||||
stoppedAtMs);
|
||||
spdlog::info("Recording segment finished: {} ({} ms)",
|
||||
this->m_currentRecordFilePath,
|
||||
stoppedAtMs - this->m_currentRecordStartedAt);
|
||||
|
||||
// If graceful stop requested, stop after finishing this segment
|
||||
if (m_stopAfterCurrentSegment.load())
|
||||
@@ -307,28 +332,58 @@ namespace snoop
|
||||
}
|
||||
}
|
||||
|
||||
// If exiting service while in a middle of a segment, ensure clean close
|
||||
// Clean shutdown if we exit while still recording
|
||||
if (m_recordingEnabled.load())
|
||||
{
|
||||
auto wallStop = std::chrono::system_clock::now();
|
||||
auto stoppedAtMs =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(wallStop.time_since_epoch()).count();
|
||||
|
||||
this->m_oggWriter->StopWriting();
|
||||
this->MoveToUploadQueue(this->m_currentRecordFilePath);
|
||||
this->MoveToUploadQueue(this->m_currentRecordFilePath,
|
||||
this->m_currentRecordStartedAt,
|
||||
stoppedAtMs);
|
||||
m_recordingEnabled = false;
|
||||
}
|
||||
}
|
||||
// Helper: ms since epoch
|
||||
static unsigned long long NowMs()
|
||||
{
|
||||
auto now = std::chrono::system_clock::now();
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
|
||||
}
|
||||
|
||||
void MoveToUploadQueue(const std::string &filePath)
|
||||
void MoveToUploadQueue(const std::string &filePath, unsigned long long startedAt, unsigned long long stoppedAt)
|
||||
{
|
||||
spdlog::info("MoveToUploadQueue( {} )", filePath);
|
||||
std::lock_guard lock(this->m_fetchFilePathsMutex);
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto recordStoppedAt = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
|
||||
if (std::filesystem::exists(filePath))
|
||||
{
|
||||
auto fileName = std::filesystem::path(filePath).filename().string() + "-" + std::to_string(recordStoppedAt);
|
||||
auto fileName = std::to_string(startedAt) + "-" + std::to_string(stoppedAt);
|
||||
std::filesystem::rename(filePath, m_queueDirectoryPath + fileName);
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy helper for startup recovery
|
||||
void MoveToUploadQueue(const std::string &filePath)
|
||||
{
|
||||
const auto stoppedAt = NowMs();
|
||||
unsigned long long startedAt = 0;
|
||||
|
||||
auto base = std::filesystem::path(filePath).filename().string();
|
||||
try
|
||||
{
|
||||
startedAt = std::stoull(base);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// if filename is not pure number, fall back to stoppedAt
|
||||
startedAt = stoppedAt;
|
||||
}
|
||||
|
||||
MoveToUploadQueue(filePath, startedAt, stoppedAt);
|
||||
}
|
||||
|
||||
void UploadThread()
|
||||
{
|
||||
const auto baseUrl = this->m_configService->GetBaseUrl();
|
||||
@@ -337,7 +392,9 @@ namespace snoop
|
||||
// certs from enrollment
|
||||
std::filesystem::path ca = "/etc/iot/keys/issuing_ca.pem";
|
||||
if (!std::filesystem::exists(ca))
|
||||
{
|
||||
ca = "/etc/iot/keys/ca_chain.pem";
|
||||
}
|
||||
const std::filesystem::path crt = "/etc/iot/keys/device.crt.pem";
|
||||
|
||||
while (!m_isIntermission)
|
||||
@@ -352,7 +409,7 @@ namespace snoop
|
||||
if (entry.is_regular_file())
|
||||
{
|
||||
files.push_back(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#include <openssl/asn1.h>
|
||||
|
||||
#include "ConfigService.h"
|
||||
#include "Security/SslCertUtil.h"
|
||||
#include "Security/TlsKeyUtil.h"
|
||||
|
||||
namespace snoop
|
||||
{
|
||||
@@ -34,7 +36,9 @@ namespace snoop
|
||||
std::string out;
|
||||
FILE *pipe = popen((cmd + " 2>&1").c_str(), "r");
|
||||
if (!pipe)
|
||||
{
|
||||
throw std::runtime_error("popen failed: " + cmd);
|
||||
}
|
||||
while (fgets(buf.data(), (int)buf.size(), pipe) != nullptr)
|
||||
{
|
||||
out.append(buf.data());
|
||||
@@ -42,17 +46,23 @@ namespace snoop
|
||||
auto rc = pclose(pipe);
|
||||
int exitCode = WIFEXITED(rc) ? WEXITSTATUS(rc) : rc;
|
||||
if (exitCode != 0)
|
||||
{
|
||||
spdlog::warn("Command '{}' exited with code {}", cmd, exitCode);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static void WriteFile(const std::filesystem::path &p, const std::string &data, bool createParents = true)
|
||||
{
|
||||
if (createParents)
|
||||
{
|
||||
std::filesystem::create_directories(p.parent_path());
|
||||
}
|
||||
std::ofstream f(p, std::ios::binary);
|
||||
if (!f)
|
||||
{
|
||||
throw std::runtime_error("Cannot open file for write: " + p.string());
|
||||
}
|
||||
f.write(data.data(), (std::streamsize)data.size());
|
||||
}
|
||||
|
||||
@@ -60,7 +70,9 @@ namespace snoop
|
||||
{
|
||||
std::ifstream f(p, std::ios::binary);
|
||||
if (!f)
|
||||
{
|
||||
throw std::runtime_error("Cannot open file for read: " + p.string());
|
||||
}
|
||||
std::ostringstream ss;
|
||||
ss << f.rdbuf();
|
||||
return ss.str();
|
||||
@@ -95,7 +107,9 @@ namespace snoop
|
||||
{
|
||||
auto serial = Trim(m[1].str());
|
||||
if (!serial.empty())
|
||||
{
|
||||
return serial;
|
||||
}
|
||||
}
|
||||
}
|
||||
spdlog::warn("CPU Serial not found, using fallback");
|
||||
@@ -125,61 +139,185 @@ namespace snoop
|
||||
|
||||
spdlog::info("Starting first-run enrollment...");
|
||||
|
||||
// 1) Run gen_device_csr.sh <GUID>
|
||||
{
|
||||
// Assumes script is in the working dir or in PATH
|
||||
const std::string cmd = "bash ./gen_device_csr.sh " + guid;
|
||||
spdlog::info("Executing: {}", cmd);
|
||||
auto out = Exec(cmd);
|
||||
spdlog::debug("gen_device_csr.sh output:\n{}", out);
|
||||
}
|
||||
// // 1) Run gen_device_csr.sh <GUID>
|
||||
// {
|
||||
// // Assumes script is in the working dir or in PATH
|
||||
// const std::string cmd = "bash ./gen_device_csr.sh " + guid;
|
||||
// spdlog::info("Executing: {}", cmd);
|
||||
// auto out = Exec(cmd);
|
||||
// spdlog::debug("gen_device_csr.sh output:\n{}", out);
|
||||
// }
|
||||
const std::string keyName = "device_" + guid + ".key";
|
||||
const std::string csrName = "device_" + guid + ".csr";
|
||||
|
||||
if (!std::filesystem::exists(keyName) || !std::filesystem::exists(csrName))
|
||||
{
|
||||
throw std::runtime_error("CSR or key was not generated by gen_device_csr.sh");
|
||||
}
|
||||
// if (!std::filesystem::exists(keyName) || !std::filesystem::exists(csrName))
|
||||
// {
|
||||
// throw std::runtime_error("CSR or key was not generated by gen_device_csr.sh");
|
||||
// }
|
||||
|
||||
// 2) CPU serial (awk '/Serial/ {print $3}' /proc/cpuinfo), if empty -> 9 * 12
|
||||
// 1) Generate key + CSR via OpenSSL (no external script)
|
||||
// {
|
||||
// spdlog::info("Generating device key + CSR via SslCertUtil");
|
||||
// snoop::device_sec::SslCertUtil::GenerateEcKeyAndCsr(
|
||||
// guid,
|
||||
// std::filesystem::path(keyName),
|
||||
// std::filesystem::path(csrName));
|
||||
// }
|
||||
|
||||
// if (!std::filesystem::exists(keyName) || !std::filesystem::exists(csrName))
|
||||
// {
|
||||
// throw std::runtime_error("CSR or key was not generated");
|
||||
// }
|
||||
|
||||
// 1) CPU serial (awk '/Serial/ {print $3}' /proc/cpuinfo), if empty -> 9 * 12
|
||||
const std::string cpuSerial = ParseCpuSerial();
|
||||
spdlog::info("CPU_SERIAL = {}", cpuSerial);
|
||||
|
||||
// 3) KEK = HMAC-SHA256(cpuSerial, key=GUID) (bash equiv used)
|
||||
// std::string kek;
|
||||
// {
|
||||
// // careful to avoid trailing newline
|
||||
// const std::string cmd = "printf %s \"" + cpuSerial + "\""
|
||||
// " | openssl dgst -sha256 -hmac \"" +
|
||||
// guid + "\" | awk '{print $2}'";
|
||||
// spdlog::info("Deriving KEK with HMAC-SHA256");
|
||||
// kek = Trim(Exec(cmd));
|
||||
// if (kek.empty())
|
||||
// throw std::runtime_error("Failed to derive KEK");
|
||||
// spdlog::debug("KEK (hex) = {}", kek);
|
||||
// }
|
||||
|
||||
// 2) KEK = HMAC-SHA256(cpuSerial, key=GUID)
|
||||
std::string kek;
|
||||
{
|
||||
// careful to avoid trailing newline
|
||||
const std::string cmd = "printf %s \"" + cpuSerial + "\""
|
||||
" | openssl dgst -sha256 -hmac \"" +
|
||||
guid + "\" | awk '{print $2}'";
|
||||
spdlog::info("Deriving KEK with HMAC-SHA256");
|
||||
kek = Trim(Exec(cmd));
|
||||
spdlog::info("Deriving KEK with HMAC-SHA256 (OpenSSL API)");
|
||||
kek = snoop::device_sec::SslCertUtil::ComputeHmacSha256Hex(guid, cpuSerial);
|
||||
if (kek.empty())
|
||||
{
|
||||
throw std::runtime_error("Failed to derive KEK");
|
||||
}
|
||||
spdlog::debug("KEK (hex) = {}", kek);
|
||||
}
|
||||
|
||||
// 4) Encrypt the private key to /etc/iot/keys/device.key.enc using KEK; shred original
|
||||
// // 4) Encrypt the private key to /etc/iot/keys/device.key.enc using KEK; shred original
|
||||
// {
|
||||
// std::filesystem::create_directories(keystoreDir);
|
||||
// const std::string cmd =
|
||||
// "openssl enc -aes-256-cbc -pbkdf2 -salt "
|
||||
// "-pass pass:" +
|
||||
// kek + " "
|
||||
// "-in " +
|
||||
// keyName + " "
|
||||
// "-out " +
|
||||
// encKeyPath.string() + " "
|
||||
// "&& shred -u " +
|
||||
// keyName;
|
||||
// spdlog::info("Encrypting private key and shredding plaintext...");
|
||||
// auto out = Exec(cmd);
|
||||
// spdlog::debug("openssl enc output:\n{}", out);
|
||||
|
||||
// if (!std::filesystem::exists(encKeyPath))
|
||||
// {
|
||||
// throw std::runtime_error("Encrypted key not created: " + encKeyPath.string());
|
||||
// }
|
||||
// }
|
||||
|
||||
// 3) Generate key + CSR via OpenSSL (no external script)
|
||||
if (!std::filesystem::exists(encKeyPath))
|
||||
{
|
||||
// ---- Fresh key: generate + encrypt ----
|
||||
spdlog::info("No encrypted key yet -> generating new EC key + CSR");
|
||||
snoop::device_sec::SslCertUtil::GenerateEcKeyAndCsr(
|
||||
guid,
|
||||
std::filesystem::path(keyName),
|
||||
std::filesystem::path(csrName));
|
||||
|
||||
if (!std::filesystem::exists(keyName) || !std::filesystem::exists(csrName))
|
||||
{
|
||||
throw std::runtime_error("CSR or key was not generated");
|
||||
}
|
||||
|
||||
std::filesystem::create_directories(keystoreDir);
|
||||
const std::string cmd =
|
||||
"openssl enc -aes-256-cbc -pbkdf2 -salt "
|
||||
"-pass pass:" +
|
||||
kek + " "
|
||||
"-in " +
|
||||
keyName + " "
|
||||
"-out " +
|
||||
encKeyPath.string() + " "
|
||||
"&& shred -u " +
|
||||
keyName;
|
||||
spdlog::info("Encrypting private key and shredding plaintext...");
|
||||
auto out = Exec(cmd);
|
||||
spdlog::debug("openssl enc output:\n{}", out);
|
||||
|
||||
spdlog::info("Encrypting private key via SslCertUtil (AES-256-CBC + PBKDF2)...");
|
||||
snoop::device_sec::SslCertUtil::EncryptFileAes256CbcPbkdf2(
|
||||
std::filesystem::path(keyName),
|
||||
encKeyPath,
|
||||
kek);
|
||||
|
||||
if (!std::filesystem::exists(encKeyPath))
|
||||
{
|
||||
throw std::runtime_error("Encrypted key not created: " + encKeyPath.string());
|
||||
}
|
||||
// shred plaintext key
|
||||
const std::string shredCmd = "shred -u " + keyName;
|
||||
spdlog::info("Shredding plaintext private key with '{}'", shredCmd);
|
||||
auto out = Exec(shredCmd);
|
||||
spdlog::debug("shred output:\n{}", out);
|
||||
}
|
||||
else
|
||||
{
|
||||
// ---- Key already exists: reuse it ----
|
||||
spdlog::info("Encrypted key already exists -> reusing key, regenerating CSR only");
|
||||
|
||||
// Decrypt to temp file (in /run/iot)
|
||||
snoop::device_sec::TempFile tf(std::filesystem::path("/run/iot"));
|
||||
snoop::device_sec::SslCertUtil::DecryptFileAes256CbcPbkdf2(
|
||||
encKeyPath,
|
||||
tf.path,
|
||||
kek);
|
||||
|
||||
// Generate CSR from existing key
|
||||
snoop::device_sec::SslCertUtil::GenerateCsrFromExistingKey(
|
||||
guid,
|
||||
tf.path,
|
||||
std::filesystem::path(csrName));
|
||||
|
||||
if (!std::filesystem::exists(csrName))
|
||||
{
|
||||
throw std::runtime_error("CSR was not generated from existing key");
|
||||
}
|
||||
// TempFile destructor will securely delete the decrypted key file
|
||||
}
|
||||
|
||||
// // 4) Encrypt the private key to /etc/iot/keys/device.key.enc using KEK; shred original
|
||||
// {
|
||||
// std::filesystem::create_directories(keystoreDir);
|
||||
|
||||
// spdlog::info("Encrypting private key via SslCertUtil (AES-256-CBC + PBKDF2)...");
|
||||
// snoop::device_sec::SslCertUtil::EncryptFileAes256CbcPbkdf2(
|
||||
// std::filesystem::path(keyName),
|
||||
// encKeyPath,
|
||||
// kek);
|
||||
|
||||
// if (!std::filesystem::exists(encKeyPath))
|
||||
// {
|
||||
// throw std::runtime_error("Encrypted key not created: " + encKeyPath.string());
|
||||
// }
|
||||
|
||||
// // still use shred to securely remove plaintext key, as requested
|
||||
// const std::string shredCmd = "shred -u " + keyName;
|
||||
// spdlog::info("Shredding plaintext private key with '{}'", shredCmd);
|
||||
// auto out = Exec(shredCmd);
|
||||
// spdlog::debug("shred output:\n{}", out);
|
||||
// }
|
||||
|
||||
// 4.5) Ensure key is loaded into kernel keyring (first creation)
|
||||
try
|
||||
{
|
||||
bool loaded = snoop::device_sec::EnsureClientKeyInKernelKeyring(guid);
|
||||
if (loaded)
|
||||
{
|
||||
spdlog::info("EnsureEnrolled: client key loaded into kernel keyring");
|
||||
}
|
||||
else
|
||||
{
|
||||
spdlog::info("EnsureEnrolled: client key was already in keyring");
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
spdlog::warn("EnsureEnrolled: failed to load key into keyring: {}", e.what());
|
||||
}
|
||||
|
||||
// 5) Send CSR to /enroll/:guid as multipart form, field `json: {"csr":"<...>"}`
|
||||
@@ -198,10 +336,15 @@ namespace snoop
|
||||
spdlog::info("POST {} (multipart)", path);
|
||||
auto res = cli.Post(path.c_str(), items);
|
||||
if (!res)
|
||||
{
|
||||
throw std::runtime_error("Enroll request failed (no response)");
|
||||
}
|
||||
spdlog::info("Enroll response status: {}", res->status);
|
||||
if (res->status != 200 && res->status != 201)
|
||||
{
|
||||
spdlog::info("Enroll error: " + res->body);
|
||||
throw std::runtime_error("Enroll failed: HTTP " + std::to_string(res->status));
|
||||
}
|
||||
|
||||
// 6) Expect JSON: { "certificate": "...", "issuing_ca":"...", "ca_chain":"..." }
|
||||
nlohmann::json j;
|
||||
@@ -214,12 +357,16 @@ namespace snoop
|
||||
throw std::runtime_error(std::string("Enroll: invalid JSON: ") + e.what());
|
||||
}
|
||||
if (!j.contains("certificate") || !j["certificate"].is_string())
|
||||
{
|
||||
throw std::runtime_error("Enroll response missing or invalid 'certificate'");
|
||||
}
|
||||
WriteFile(certPath, j["certificate"].get<std::string>());
|
||||
if (j.contains("issuing_ca"))
|
||||
{
|
||||
if (!j["issuing_ca"].is_string())
|
||||
{
|
||||
throw std::runtime_error("'issuing_ca' must be a string PEM");
|
||||
}
|
||||
WriteFile(caPath, j["issuing_ca"].get<std::string>());
|
||||
}
|
||||
|
||||
@@ -287,13 +434,31 @@ namespace snoop
|
||||
}
|
||||
|
||||
// ----- 2) re-generate CSR via your bash script -----
|
||||
{
|
||||
const std::string cmd = "bash ./gen_device_csr.sh " + guid;
|
||||
spdlog::info("Executing (renew): {}", cmd);
|
||||
auto out = Exec(cmd);
|
||||
spdlog::debug("gen_device_csr.sh (renew) output:\n{}", out);
|
||||
}
|
||||
// {
|
||||
// const std::string cmd = "bash ./gen_device_csr.sh " + guid;
|
||||
// spdlog::info("Executing (renew): {}", cmd);
|
||||
// auto out = Exec(cmd);
|
||||
// spdlog::debug("gen_device_csr.sh (renew) output:\n{}", out);
|
||||
// }
|
||||
// const std::string csrName = "device_" + guid + ".csr";
|
||||
// if (!std::filesystem::exists(csrName))
|
||||
// {
|
||||
// throw std::runtime_error("Renew: CSR was not generated");
|
||||
// }
|
||||
// std::string csrPem = ReadFile(csrName);
|
||||
|
||||
// ----- 2) re-generate CSR via OpenSSL (no bash) -----
|
||||
const std::string csrName = "device_" + guid + ".csr";
|
||||
{
|
||||
spdlog::info("RenewCertificate: generating new CSR via SslCertUtil");
|
||||
// We must reuse the existing key or generate a new one?
|
||||
// Current flow (bash script) always regenerated key+csr, so we mimic that:
|
||||
snoop::device_sec::SslCertUtil::GenerateEcKeyAndCsr(
|
||||
guid,
|
||||
std::filesystem::path("device_" + guid + ".key"),
|
||||
std::filesystem::path(csrName));
|
||||
}
|
||||
|
||||
if (!std::filesystem::exists(csrName))
|
||||
{
|
||||
throw std::runtime_error("Renew: CSR was not generated");
|
||||
@@ -315,15 +480,21 @@ namespace snoop
|
||||
auto res = cli->Post(path.c_str(), items);
|
||||
|
||||
if (!res)
|
||||
{
|
||||
throw std::runtime_error("Renew request failed (no response)");
|
||||
}
|
||||
spdlog::info("Renew response status: {}", res->status);
|
||||
if (res->status != 200)
|
||||
{
|
||||
throw std::runtime_error("Renew failed: HTTP " + std::to_string(res->status));
|
||||
}
|
||||
|
||||
nlohmann::json j = nlohmann::json::parse(res->body);
|
||||
|
||||
if (!j.contains("certificate") || !j["certificate"].is_string())
|
||||
{
|
||||
throw std::runtime_error("Renew response missing or invalid 'certificate'");
|
||||
}
|
||||
WriteFile(certPath, j["certificate"].get<std::string>());
|
||||
|
||||
if (j.contains("issuing_ca") && j["issuing_ca"].is_string())
|
||||
@@ -335,11 +506,17 @@ namespace snoop
|
||||
{
|
||||
std::string chainPem;
|
||||
if (j["ca_chain"].is_string())
|
||||
{
|
||||
chainPem = j["ca_chain"].get<std::string>();
|
||||
}
|
||||
else if (j["ca_chain"].is_array())
|
||||
{
|
||||
chainPem = JoinPemArray(j["ca_chain"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("'ca_chain' must be string or array");
|
||||
}
|
||||
WriteFile(chainPath, chainPem);
|
||||
}
|
||||
|
||||
@@ -385,7 +562,9 @@ namespace snoop
|
||||
{
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
if (!f)
|
||||
{
|
||||
throw std::runtime_error("Failed to open file: " + path);
|
||||
}
|
||||
std::ostringstream ss;
|
||||
ss << f.rdbuf();
|
||||
return ss.str();
|
||||
@@ -461,7 +640,7 @@ namespace snoop
|
||||
if (days == 0 && secs <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return days <= daysThreshold;
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,9 @@ namespace snoop
|
||||
{
|
||||
std::lock_guard lk(m_mtx);
|
||||
if (m_started)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse & normalize the provided URL now (extract token, add trailing /whip)
|
||||
m_endpoint = ParseWhipUrl(m_p.whipUrl);
|
||||
@@ -97,11 +99,12 @@ namespace snoop
|
||||
spdlog::warn("WHIP PATCH candidate failed: {}", e.what());
|
||||
} });
|
||||
|
||||
// 2) When ICE gathering completes, send end-of-candidates
|
||||
m_pc->onGatheringStateChange([this](rtc::PeerConnection::GatheringState s)
|
||||
// 2) When ICE gathering completes, send end-of-candidates
|
||||
m_pc->onGatheringStateChange([this](rtc::PeerConnection::GatheringState s)
|
||||
{
|
||||
spdlog::info("WHIP gathering state: {}", (int)s);
|
||||
if (s == rtc::PeerConnection::GatheringState::Complete) {
|
||||
if (s == rtc::PeerConnection::GatheringState::Complete)
|
||||
{
|
||||
PatchSdpFrag("a=end-of-candidates");
|
||||
} });
|
||||
|
||||
@@ -136,7 +139,9 @@ namespace snoop
|
||||
{
|
||||
std::lock_guard lk(m_mtx);
|
||||
if (!m_started)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_trackOpen = false;
|
||||
m_track.reset();
|
||||
@@ -150,7 +155,9 @@ namespace snoop
|
||||
{
|
||||
std::lock_guard lk(m_mtx);
|
||||
if (!m_track || !m_started || !m_trackOpen.load())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::array<uint8_t, 12> rtp{};
|
||||
rtp[0] = 0x80; // V=2
|
||||
@@ -233,7 +240,9 @@ namespace snoop
|
||||
{
|
||||
size_t amp = query.find('&', pos);
|
||||
if (amp == std::string::npos)
|
||||
{
|
||||
amp = query.size();
|
||||
}
|
||||
auto kv = query.substr(pos, amp - pos);
|
||||
auto eq = kv.find('=');
|
||||
if (eq != std::string::npos)
|
||||
@@ -252,9 +261,13 @@ namespace snoop
|
||||
// MediaMTX wants: /whip/<original...>/whip
|
||||
// Your incoming URLs already start with /whip/... — we just ensure they end with /whip.
|
||||
if (rawPath.empty())
|
||||
{
|
||||
rawPath = "/";
|
||||
}
|
||||
if (rawPath.back() == '/')
|
||||
{
|
||||
rawPath.pop_back();
|
||||
}
|
||||
if (rawPath.rfind("/whip", std::string::npos) != rawPath.size() - 5)
|
||||
{
|
||||
rawPath += "/whip";
|
||||
@@ -266,7 +279,9 @@ namespace snoop
|
||||
static std::tuple<std::string, std::string> ExtractAnswerAndLocation(const httplib::Result &r)
|
||||
{
|
||||
if (!r)
|
||||
{
|
||||
throw std::runtime_error("No HTTP result");
|
||||
}
|
||||
if (r->status != 201 && r->status != 200)
|
||||
{
|
||||
throw std::runtime_error("Unexpected WHIP status: " + std::to_string(r->status));
|
||||
@@ -274,7 +289,9 @@ namespace snoop
|
||||
std::string answer = r->body;
|
||||
std::string resourceUrl;
|
||||
if (r->has_header("Location"))
|
||||
{
|
||||
resourceUrl = r->get_header_value("Location");
|
||||
}
|
||||
return {answer, resourceUrl};
|
||||
}
|
||||
|
||||
@@ -294,7 +311,9 @@ namespace snoop
|
||||
|
||||
auto res = cli->Post(m_endpoint.path.c_str(), hs, sdpOffer, "application/sdp");
|
||||
if (!res)
|
||||
{
|
||||
throw std::runtime_error("No HTTP result (network?)");
|
||||
}
|
||||
|
||||
const auto ctype = res->get_header_value("Content-Type");
|
||||
const bool has_loc = res->has_header("Location");
|
||||
@@ -313,7 +332,9 @@ namespace snoop
|
||||
std::string answer = res->body;
|
||||
std::string resourceUrl;
|
||||
if (res->has_header("Location"))
|
||||
{
|
||||
resourceUrl = res->get_header_value("Location");
|
||||
}
|
||||
|
||||
if (answer.find("a=ice-ufrag:") == std::string::npos)
|
||||
{
|
||||
@@ -334,7 +355,9 @@ namespace snoop
|
||||
// If MediaMTX returns an absolute Location for the resource, we parse it;
|
||||
// otherwise we reuse the same host/port and use Location as path.
|
||||
if (!m_resourceUrl)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ParsedUrl target;
|
||||
try
|
||||
@@ -393,7 +416,9 @@ namespace snoop
|
||||
void PatchSdpFrag(const std::string &sdpfrag)
|
||||
{
|
||||
if (!m_resourceUrl)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Reuse the same host/port and just set path = Location
|
||||
ParsedUrl target = m_endpoint;
|
||||
@@ -411,7 +436,9 @@ namespace snoop
|
||||
// MUST include CRLF at end of body
|
||||
std::string body = sdpfrag;
|
||||
if (body.empty() || body.back() != '\n')
|
||||
{
|
||||
body += "\r\n";
|
||||
}
|
||||
|
||||
auto res = cli->Patch(target.path.c_str(), hs, body, "application/trickle-ice-sdpfrag");
|
||||
if (!res || !(res->status == 200 || res->status == 201 || res->status == 204))
|
||||
|
||||
39
src/main.cpp
39
src/main.cpp
@@ -10,6 +10,7 @@
|
||||
#include "Services/ConfigService.h"
|
||||
#include "Services/EnrollmentService.h"
|
||||
#include "Services/DeviceControlService.h"
|
||||
#include "Security/TlsKeyUtil.h"
|
||||
|
||||
#ifdef USE_ALSA_ADAPTER
|
||||
#include "AudioAdapters/AlsaAudioAdapter.h"
|
||||
@@ -35,22 +36,40 @@ namespace snoop
|
||||
auto configService = std::make_shared<ConfigService>("config.json");
|
||||
|
||||
// ---- FIRST-RUN ENROLLMENT ----
|
||||
// EnrollmentService enroll(configService);
|
||||
// const bool didEnroll = enroll.EnsureEnrolled();
|
||||
// if (didEnroll)
|
||||
// {
|
||||
// spdlog::info("First-run enrollment completed.");
|
||||
// }
|
||||
auto enrollSvc = std::make_shared<EnrollmentService>(configService);
|
||||
enrollSvc->EnsureEnrolled();
|
||||
try
|
||||
{
|
||||
EnrollmentService enroll(configService);
|
||||
const bool didEnroll = enroll.EnsureEnrolled();
|
||||
if (didEnroll)
|
||||
enrollSvc->RenewCertificate(false);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
spdlog::warn("Auto-renew check failed: {}", e.what());
|
||||
}
|
||||
|
||||
// Ensure client key is in kernel keyring for this session
|
||||
try
|
||||
{
|
||||
bool loaded = snoop::device_sec::EnsureClientKeyInKernelKeyring(configService->GetGuid());
|
||||
if (loaded)
|
||||
{
|
||||
spdlog::info("First-run enrollment completed.");
|
||||
spdlog::info("Main: client key loaded into kernel keyring for this session");
|
||||
}
|
||||
try
|
||||
else
|
||||
{
|
||||
enroll.RenewCertificate(false);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
spdlog::warn("Auto-renew check failed: {}", e.what());
|
||||
spdlog::info("Main: client key already present in kernel keyring");
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
spdlog::warn("Main: failed to ensure client key in keyring: {}", e.what());
|
||||
}
|
||||
|
||||
auto writerService = std::make_shared<AudioWriterService>(configService, "records");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user