|
11 | 11 | #include <algorithm> |
12 | 12 | #include <cctype> |
13 | 13 | #include <chrono> |
| 14 | +#include <cstdio> |
14 | 15 | #include <cstdlib> |
| 16 | +#include <ctime> |
15 | 17 | #include <fstream> |
| 18 | +#include <iomanip> |
16 | 19 | #include <map> |
17 | 20 | #include <memory> |
18 | 21 | #include <sstream> |
19 | 22 | #include <stdexcept> |
20 | 23 |
|
21 | 24 | #include <sodium.h> |
22 | 25 |
|
| 26 | +#ifdef _WIN32 |
| 27 | +#include <windows.h> |
| 28 | +#else |
| 29 | +#include <unistd.h> |
| 30 | +#endif |
| 31 | + |
23 | 32 | namespace authforge { |
24 | 33 |
|
25 | 34 | namespace { |
@@ -794,4 +803,230 @@ void AuthForgeClient::ApplyOfflineLicense(const OfflineLicense &license) { |
794 | 803 | authenticated_ = true; |
795 | 804 | } |
796 | 805 |
|
| 806 | +namespace { |
| 807 | + |
| 808 | +constexpr int kArmorLineWidth = 64; |
| 809 | +constexpr int kMaxRequestHwid = 256; |
| 810 | +constexpr int kMaxRequestMachineName = 128; |
| 811 | +constexpr int kMaxRequestOs = 64; |
| 812 | +constexpr int kMaxRequestSdk = 64; |
| 813 | +constexpr int kMaxRequestLicenseKey = 64; |
| 814 | +constexpr const char *kActivationRequestTyp = "authforge-activation-request"; |
| 815 | +constexpr const char *kBeginActivationRequest = "-----BEGIN AUTHFORGE ACTIVATION REQUEST-----"; |
| 816 | +constexpr const char *kEndActivationRequest = "-----END AUTHFORGE ACTIVATION REQUEST-----"; |
| 817 | +constexpr const char *kActivationRequestSdkTag = "cpp/1.2.1"; |
| 818 | + |
| 819 | +std::string ClipRequestField(const std::string &value, int max) { |
| 820 | + if (static_cast<int>(value.size()) <= max) { |
| 821 | + return value; |
| 822 | + } |
| 823 | + return value.substr(0, static_cast<std::size_t>(max)); |
| 824 | +} |
| 825 | + |
| 826 | +std::string JsonEscapeRequest(const std::string &value) { |
| 827 | + std::ostringstream oss; |
| 828 | + oss << '"'; |
| 829 | + for (unsigned char ch : value) { |
| 830 | + switch (ch) { |
| 831 | + case '\\': |
| 832 | + oss << "\\\\"; |
| 833 | + break; |
| 834 | + case '"': |
| 835 | + oss << "\\\""; |
| 836 | + break; |
| 837 | + case '\b': |
| 838 | + oss << "\\b"; |
| 839 | + break; |
| 840 | + case '\f': |
| 841 | + oss << "\\f"; |
| 842 | + break; |
| 843 | + case '\n': |
| 844 | + oss << "\\n"; |
| 845 | + break; |
| 846 | + case '\r': |
| 847 | + oss << "\\r"; |
| 848 | + break; |
| 849 | + case '\t': |
| 850 | + oss << "\\t"; |
| 851 | + break; |
| 852 | + default: |
| 853 | + if (ch < 0x20U) { |
| 854 | + oss << "\\u00"; |
| 855 | + oss << "0123456789abcdef"[(ch >> 4) & 0x0F]; |
| 856 | + oss << "0123456789abcdef"[ch & 0x0F]; |
| 857 | + } else { |
| 858 | + oss << static_cast<char>(ch); |
| 859 | + } |
| 860 | + break; |
| 861 | + } |
| 862 | + } |
| 863 | + oss << '"'; |
| 864 | + return oss.str(); |
| 865 | +} |
| 866 | + |
| 867 | +std::string WrapArmor64(const std::string &value) { |
| 868 | + std::string out; |
| 869 | + for (std::size_t i = 0; i < value.size(); i += static_cast<std::size_t>(kArmorLineWidth)) { |
| 870 | + if (!out.empty()) { |
| 871 | + out.push_back('\n'); |
| 872 | + } |
| 873 | + out.append(value, i, static_cast<std::size_t>(kArmorLineWidth)); |
| 874 | + } |
| 875 | + return out; |
| 876 | +} |
| 877 | + |
| 878 | +std::string EncodeBase64(const std::string &input) { |
| 879 | + static const char kTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; |
| 880 | + std::string out; |
| 881 | + const auto *data = reinterpret_cast<const unsigned char *>(input.data()); |
| 882 | + const std::size_t len = input.size(); |
| 883 | + out.reserve(((len + 2) / 3) * 4); |
| 884 | + std::size_t i = 0; |
| 885 | + while (i + 2 < len) { |
| 886 | + const unsigned int n = (static_cast<unsigned int>(data[i]) << 16) | (static_cast<unsigned int>(data[i + 1]) << 8) | |
| 887 | + static_cast<unsigned int>(data[i + 2]); |
| 888 | + out.push_back(kTable[(n >> 18) & 63]); |
| 889 | + out.push_back(kTable[(n >> 12) & 63]); |
| 890 | + out.push_back(kTable[(n >> 6) & 63]); |
| 891 | + out.push_back(kTable[n & 63]); |
| 892 | + i += 3; |
| 893 | + } |
| 894 | + if (i < len) { |
| 895 | + unsigned int n = static_cast<unsigned int>(data[i]) << 16; |
| 896 | + if (i + 1 < len) { |
| 897 | + n |= static_cast<unsigned int>(data[i + 1]) << 8; |
| 898 | + } |
| 899 | + out.push_back(kTable[(n >> 18) & 63]); |
| 900 | + out.push_back(kTable[(n >> 12) & 63]); |
| 901 | + out.push_back(i + 1 < len ? kTable[(n >> 6) & 63] : '='); |
| 902 | + out.push_back('='); |
| 903 | + } |
| 904 | + return out; |
| 905 | +} |
| 906 | + |
| 907 | +std::string Sha256Hex16(const std::string &payloadB64) { |
| 908 | + if (sodium_init() < 0) { |
| 909 | + throw std::runtime_error("sodium_init failed"); |
| 910 | + } |
| 911 | + unsigned char digest[crypto_hash_sha256_BYTES]; |
| 912 | + crypto_hash_sha256(digest, reinterpret_cast<const unsigned char *>(payloadB64.data()), payloadB64.size()); |
| 913 | + static constexpr char kHex[] = "0123456789abcdef"; |
| 914 | + std::string out(16, '0'); |
| 915 | + for (int i = 0; i < 8; ++i) { |
| 916 | + out[static_cast<std::size_t>(i * 2)] = kHex[(digest[i] >> 4) & 0x0F]; |
| 917 | + out[static_cast<std::size_t>(i * 2 + 1)] = kHex[digest[i] & 0x0F]; |
| 918 | + } |
| 919 | + return out; |
| 920 | +} |
| 921 | + |
| 922 | +std::string CanonicalActivationRequestJson(const std::string &appId, const std::string &hwid, const std::string &createdAt, |
| 923 | + const std::string &machineName, const std::string &os, const std::string &sdk, |
| 924 | + const std::string &licenseKey) { |
| 925 | + std::string json = "{"; |
| 926 | + json += "\"v\":1"; |
| 927 | + json += ",\"typ\":" + JsonEscapeRequest(kActivationRequestTyp); |
| 928 | + json += ",\"appId\":" + JsonEscapeRequest(appId); |
| 929 | + json += ",\"hwid\":" + JsonEscapeRequest(ClipRequestField(hwid, kMaxRequestHwid)); |
| 930 | + json += ",\"createdAt\":" + JsonEscapeRequest(createdAt); |
| 931 | + if (!machineName.empty()) { |
| 932 | + json += ",\"machineName\":" + JsonEscapeRequest(ClipRequestField(machineName, kMaxRequestMachineName)); |
| 933 | + } |
| 934 | + if (!os.empty()) { |
| 935 | + json += ",\"os\":" + JsonEscapeRequest(ClipRequestField(os, kMaxRequestOs)); |
| 936 | + } |
| 937 | + if (!sdk.empty()) { |
| 938 | + json += ",\"sdk\":" + JsonEscapeRequest(ClipRequestField(sdk, kMaxRequestSdk)); |
| 939 | + } |
| 940 | + if (!licenseKey.empty()) { |
| 941 | + json += ",\"licenseKey\":" + JsonEscapeRequest(ClipRequestField(licenseKey, kMaxRequestLicenseKey)); |
| 942 | + } |
| 943 | + json += "}"; |
| 944 | + return json; |
| 945 | +} |
| 946 | + |
| 947 | +std::string DetectOsLabel() { |
| 948 | +#if defined(_WIN32) |
| 949 | + return "Windows"; |
| 950 | +#elif defined(__APPLE__) |
| 951 | + return "macOS"; |
| 952 | +#else |
| 953 | + return "Linux"; |
| 954 | +#endif |
| 955 | +} |
| 956 | + |
| 957 | +std::string UtcIsoMsNow() { |
| 958 | + const auto now = std::chrono::system_clock::now(); |
| 959 | + const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000; |
| 960 | + const std::time_t t = std::chrono::system_clock::to_time_t(now); |
| 961 | + std::tm tm{}; |
| 962 | +#ifdef _WIN32 |
| 963 | + gmtime_s(&tm, &t); |
| 964 | +#else |
| 965 | + gmtime_r(&t, &tm); |
| 966 | +#endif |
| 967 | + char buf[32]; |
| 968 | + std::snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, |
| 969 | + tm.tm_hour, tm.tm_min, tm.tm_sec, static_cast<int>(ms.count())); |
| 970 | + return buf; |
| 971 | +} |
| 972 | + |
| 973 | +std::string DetectHostname() { |
| 974 | +#ifdef _WIN32 |
| 975 | + char buf[256]; |
| 976 | + DWORD n = static_cast<DWORD>(sizeof(buf)); |
| 977 | + if (GetComputerNameA(buf, &n) != 0) { |
| 978 | + return buf; |
| 979 | + } |
| 980 | +#else |
| 981 | + char buf[256]; |
| 982 | + if (gethostname(buf, sizeof(buf)) == 0) { |
| 983 | + return buf; |
| 984 | + } |
| 985 | +#endif |
| 986 | + return ""; |
| 987 | +} |
| 988 | + |
| 989 | +} // namespace |
| 990 | + |
| 991 | +std::string FormatActivationRequest(const std::string &appId, const std::string &hwid, const std::string &createdAt, |
| 992 | + const std::string &machineName, const std::string &os, const std::string &sdk, |
| 993 | + const std::string &licenseKey) { |
| 994 | + const std::string json = CanonicalActivationRequestJson(appId, hwid, createdAt, machineName, os, sdk, licenseKey); |
| 995 | + const std::string payloadB64 = EncodeBase64(json); |
| 996 | + const std::string checksum = Sha256Hex16(payloadB64); |
| 997 | + std::string clean = appId; |
| 998 | + for (char &ch : clean) { |
| 999 | + if (ch == '\r' || ch == '\n') { |
| 1000 | + ch = ' '; |
| 1001 | + } |
| 1002 | + } |
| 1003 | + std::ostringstream oss; |
| 1004 | + oss << kBeginActivationRequest << "\n"; |
| 1005 | + oss << "Version: 1\n"; |
| 1006 | + oss << "App-Id: " << TrimCopy(clean) << "\n"; |
| 1007 | + oss << "Checksum: " << checksum << "\n"; |
| 1008 | + oss << "\n"; |
| 1009 | + oss << WrapArmor64(payloadB64) << "\n"; |
| 1010 | + oss << kEndActivationRequest << "\n"; |
| 1011 | + return oss.str(); |
| 1012 | +} |
| 1013 | + |
| 1014 | +std::string AuthForgeClient::CreateActivationRequest(const ActivationRequestOptions &options) const { |
| 1015 | + const std::string createdAt = options.createdAt.empty() ? UtcIsoMsNow() : options.createdAt; |
| 1016 | + std::string machineName; |
| 1017 | + if (options.includeMachineName) { |
| 1018 | + machineName = options.machineName.empty() ? DetectHostname() : options.machineName; |
| 1019 | + } |
| 1020 | + std::string os; |
| 1021 | + if (!options.omitOs) { |
| 1022 | + os = options.os.empty() ? DetectOsLabel() : options.os; |
| 1023 | + } |
| 1024 | + std::string sdk; |
| 1025 | + if (!options.omitSdk) { |
| 1026 | + sdk = options.sdk.empty() ? kActivationRequestSdkTag : options.sdk; |
| 1027 | + } |
| 1028 | + const std::string licenseKey = options.licenseKey.empty() ? licenseKey_ : options.licenseKey; |
| 1029 | + return FormatActivationRequest(appId_, hwid_, createdAt, machineName, os, sdk, licenseKey); |
| 1030 | +} |
| 1031 | + |
797 | 1032 | } // namespace authforge |
0 commit comments