#include "licence.hpp" #include #include #include #include #if defined(LICENCE_BACKEND_SODIUM) #include #else #include #endif namespace letissier { namespace { std::vector from_hex(const std::string& hex) { std::vector out; if (hex.size() % 2 != 0) { return out; } out.reserve(hex.size() / 2); for (std::size_t index = 0; index < hex.size(); index += 2) { auto nibble = [](char c) -> int { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; }; const int high = nibble(hex[index]); const int low = nibble(hex[index + 1]); if (high < 0 || low < 0) { return {}; } out.push_back(static_cast((high << 4) | low)); } return out; } std::string to_hex(const unsigned char* data, std::size_t length) { static const char* digits = "0123456789abcdef"; std::string out; out.reserve(length * 2); for (std::size_t index = 0; index < length; ++index) { out.push_back(digits[data[index] >> 4]); out.push_back(digits[data[index] & 0x0F]); } return out; } // base64url, no padding. std::vector b64url_decode(const std::string& input) { auto value = [](char c) -> int { if (c >= 'A' && c <= 'Z') return c - 'A'; if (c >= 'a' && c <= 'z') return c - 'a' + 26; if (c >= '0' && c <= '9') return c - '0' + 52; if (c == '-') return 62; if (c == '_') return 63; return -1; }; std::vector out; int buffer = 0; int bits = 0; for (const char c : input) { const int digit = value(c); if (digit < 0) { return {}; } buffer = (buffer << 6) | digit; bits += 6; if (bits >= 8) { bits -= 8; out.push_back(static_cast((buffer >> bits) & 0xFF)); } } return out; } bool ed25519_verify(const unsigned char* message, std::size_t message_length, const std::vector& signature, const std::vector& key) { if (signature.size() != 64 || key.size() != 32) { return false; } #if defined(LICENCE_BACKEND_SODIUM) return crypto_sign_verify_detached(signature.data(), message, message_length, key.data()) == 0; #else EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, nullptr, key.data(), key.size()); if (pkey == nullptr) { return false; } EVP_MD_CTX* ctx = EVP_MD_CTX_new(); bool ok = false; if (ctx != nullptr && EVP_DigestVerifyInit(ctx, nullptr, nullptr, nullptr, pkey) == 1) { ok = EVP_DigestVerify(ctx, signature.data(), signature.size(), message, message_length) == 1; } if (ctx != nullptr) { EVP_MD_CTX_free(ctx); } EVP_PKEY_free(pkey); return ok; #endif } void sha256(const std::string& input, unsigned char out[32]) { #if defined(LICENCE_BACKEND_SODIUM) crypto_hash_sha256(out, reinterpret_cast(input.data()), input.size()); #else unsigned int length = 0; EVP_Digest(input.data(), input.size(), out, &length, EVP_sha256(), nullptr); #endif } std::string trim(const std::string& input) { const auto begin = std::find_if_not(input.begin(), input.end(), [](unsigned char c) { return std::isspace(c); }); const auto end = std::find_if_not(input.rbegin(), input.rend(), [](unsigned char c) { return std::isspace(c); }) .base(); return begin < end ? std::string(begin, end) : std::string(); } // Minimal JSON reading for the flat claims object. The payload is signed // before it is parsed, so this only ever sees data the studio produced. std::string json_string(const std::string& json, const std::string& field) { const std::string needle = "\"" + field + "\""; const auto at = json.find(needle); if (at == std::string::npos) { return {}; } auto cursor = json.find(':', at + needle.size()); if (cursor == std::string::npos) { return {}; } cursor = json.find('"', cursor); if (cursor == std::string::npos) { return {}; } const auto end = json.find('"', cursor + 1); return end == std::string::npos ? std::string() : json.substr(cursor + 1, end - cursor - 1); } std::int64_t json_number(const std::string& json, const std::string& field) { const std::string needle = "\"" + field + "\""; const auto at = json.find(needle); if (at == std::string::npos) { return 0; } auto cursor = json.find(':', at + needle.size()); if (cursor == std::string::npos) { return 0; } ++cursor; while (cursor < json.size() && std::isspace(static_cast(json[cursor]))) { ++cursor; } return std::strtoll(json.c_str() + cursor, nullptr, 10); } } // namespace const char* to_string(Status status) { switch (status) { case Status::Active: return "active"; case Status::UpdateRequired: return "update_required"; case Status::CheckInRequired: return "check_in_required"; case Status::Expired: return "expired"; case Status::WrongMachine: return "wrong_machine"; case Status::Invalid: return "invalid"; } return "invalid"; } std::string machine_hash(const std::string& fingerprint) { unsigned char digest[32]; sha256(trim(fingerprint), digest); return to_hex(digest, sizeof(digest)).substr(0, 32); } bool verify(const std::string& token, const std::string& public_key_hex, Claims& out) { const auto dot = token.find('.'); if (dot == std::string::npos || token.find('.', dot + 1) != std::string::npos) { return false; } const std::string payload = token.substr(0, dot); const std::string signature = token.substr(dot + 1); if (!ed25519_verify(reinterpret_cast(payload.data()), payload.size(), b64url_decode(signature), from_hex(public_key_hex))) { return false; } const auto decoded = b64url_decode(payload); const std::string json(decoded.begin(), decoded.end()); out.version = static_cast(json_number(json, "v")); if (out.version != 1) { return false; } out.key = json_string(json, "key"); out.product = json_string(json, "product"); out.edition = json_string(json, "edition"); out.customer = json_string(json, "customer"); out.name = json_string(json, "name"); out.seats = static_cast(json_number(json, "seats")); out.maint_until = json_number(json, "maintUntil"); out.exp = json_number(json, "exp"); out.machine = json_string(json, "machine"); out.mode = json_string(json, "mode"); out.iat = json_number(json, "iat"); out.jti = json_string(json, "jti"); return true; } Verdict check(const std::string& token, const std::string& fingerprint, std::int64_t build_date, std::int64_t now, const std::string& public_key_hex) { Verdict verdict; if (!verify(token, public_key_hex, verdict.claims)) { verdict.status = Status::Invalid; return verdict; } verdict.has_claims = true; if (verdict.claims.machine != machine_hash(fingerprint)) { verdict.status = Status::WrongMachine; return verdict; } verdict.check_in_in = verdict.claims.exp - now; if (verdict.check_in_in <= 0) { // A trial's lease is its lifetime, so a lapsed trial is simply over. verdict.status = verdict.claims.edition == "trial" ? Status::Expired : Status::CheckInRequired; return verdict; } verdict.status = build_date > verdict.claims.maint_until ? Status::UpdateRequired : Status::Active; return verdict; } } // namespace letissier