Skip to content
API Documentation
BastionGuard™
activation.cpp
Go to the documentation of this file.
1/*
2 * BastionGuard™ Recovery Data
3 * Copyright (C) 2025–2026 Calogero Scarnà. All rights reserved.
4 *
5 * Proprietary activation support for donation-based distribution.
6 *
7 * SECURITY NOTE FOR THE OWNER:
8 * This is an offline activation system bound to /etc/machine-id and motherboard serial. Before a public release, change
9 * kActivationSecret in both this file and tools/bastionguard_keygen.py,
10 * keep the keygen private, and ship only the application binary to users.
11 */
12
13#include "activation.hpp"
14
15#include <array>
16#include <algorithm>
17#include <cctype>
18#include <chrono>
19#include <cstdint>
20#include <cstdlib>
21#include <ctime>
22#include <filesystem>
23#include <fstream>
24#include <iomanip>
25#include <map>
26#include <optional>
27#include <sstream>
28#include <string_view>
29#include <vector>
30#include <unistd.h>
31
33namespace {
34
35// Change this secret before distributing production builds.
36// The same value must be used by tools/bastionguard_keygen.py.
37constexpr std::string_view kActivationSecret =
38 "BastionGuard-Recovery-Data-2026-Calogero-Scarna-Donation-Activation-v1";
39
40constexpr std::string_view kProductId = "BastionGuardRecoveryData";
41// Fixed shared activation engine ID. Keep this exact value aligned with
42// tools/bastionguard_keygen.py. It is intentionally human-readable so the
43// app and keygen cannot diverge because of hash truncation or formatting.
44constexpr std::string_view kActivationEngineId = "supportterdonation";
45constexpr std::string_view kKeyPrefixV1 = "BGRD1";
46constexpr std::string_view kKeyPrefixV2 = "BGRD2";
47constexpr std::string_view kKeyPrefixV3 = "BGRD3";
48constexpr std::string_view kKeyBeginMarker = "-----BEGIN BASTIONGUARD ACTIVATION KEY-----";
49constexpr std::string_view kKeyEndMarker = "-----END BASTIONGUARD ACTIVATION KEY-----";
50
51uint32_t rotr(uint32_t value, unsigned int count) {
52 return (value >> count) | (value << (32U - count));
53}
54
55std::string sha256_hex(const std::string& input) {
56 static constexpr std::array<uint32_t, 64> k = {
57 0x428a2f98U, 0x71374491U, 0xb5c0fbcfU, 0xe9b5dba5U,
58 0x3956c25bU, 0x59f111f1U, 0x923f82a4U, 0xab1c5ed5U,
59 0xd807aa98U, 0x12835b01U, 0x243185beU, 0x550c7dc3U,
60 0x72be5d74U, 0x80deb1feU, 0x9bdc06a7U, 0xc19bf174U,
61 0xe49b69c1U, 0xefbe4786U, 0x0fc19dc6U, 0x240ca1ccU,
62 0x2de92c6fU, 0x4a7484aaU, 0x5cb0a9dcU, 0x76f988daU,
63 0x983e5152U, 0xa831c66dU, 0xb00327c8U, 0xbf597fc7U,
64 0xc6e00bf3U, 0xd5a79147U, 0x06ca6351U, 0x14292967U,
65 0x27b70a85U, 0x2e1b2138U, 0x4d2c6dfcU, 0x53380d13U,
66 0x650a7354U, 0x766a0abbU, 0x81c2c92eU, 0x92722c85U,
67 0xa2bfe8a1U, 0xa81a664bU, 0xc24b8b70U, 0xc76c51a3U,
68 0xd192e819U, 0xd6990624U, 0xf40e3585U, 0x106aa070U,
69 0x19a4c116U, 0x1e376c08U, 0x2748774cU, 0x34b0bcb5U,
70 0x391c0cb3U, 0x4ed8aa4aU, 0x5b9cca4fU, 0x682e6ff3U,
71 0x748f82eeU, 0x78a5636fU, 0x84c87814U, 0x8cc70208U,
72 0x90befffaU, 0xa4506cebU, 0xbef9a3f7U, 0xc67178f2U
73 };
74
75 std::array<uint32_t, 8> h = {
76 0x6a09e667U, 0xbb67ae85U, 0x3c6ef372U, 0xa54ff53aU,
77 0x510e527fU, 0x9b05688cU, 0x1f83d9abU, 0x5be0cd19U
78 };
79
80 std::vector<uint8_t> msg(input.begin(), input.end());
81 const uint64_t bit_len = static_cast<uint64_t>(msg.size()) * 8ULL;
82
83 msg.push_back(0x80U);
84 while ((msg.size() % 64U) != 56U) {
85 msg.push_back(0x00U);
86 }
87
88 for (int i = 7; i >= 0; --i) {
89 msg.push_back(static_cast<uint8_t>((bit_len >> (i * 8)) & 0xffU));
90 }
91
92 for (size_t offset = 0; offset < msg.size(); offset += 64) {
93 std::array<uint32_t, 64> w{};
94 for (size_t i = 0; i < 16; ++i) {
95 const size_t j = offset + i * 4;
96 w[i] = (static_cast<uint32_t>(msg[j]) << 24U) |
97 (static_cast<uint32_t>(msg[j + 1]) << 16U) |
98 (static_cast<uint32_t>(msg[j + 2]) << 8U) |
99 static_cast<uint32_t>(msg[j + 3]);
100 }
101
102 for (size_t i = 16; i < 64; ++i) {
103 const uint32_t s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >> 3U);
104 const uint32_t s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >> 10U);
105 w[i] = w[i - 16] + s0 + w[i - 7] + s1;
106 }
107
108 uint32_t a = h[0];
109 uint32_t b = h[1];
110 uint32_t c = h[2];
111 uint32_t d = h[3];
112 uint32_t e = h[4];
113 uint32_t f = h[5];
114 uint32_t g = h[6];
115 uint32_t hh = h[7];
116
117 for (size_t i = 0; i < 64; ++i) {
118 const uint32_t s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
119 const uint32_t ch = (e & f) ^ ((~e) & g);
120 const uint32_t temp1 = hh + s1 + ch + k[i] + w[i];
121 const uint32_t s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
122 const uint32_t maj = (a & b) ^ (a & c) ^ (b & c);
123 const uint32_t temp2 = s0 + maj;
124
125 hh = g;
126 g = f;
127 f = e;
128 e = d + temp1;
129 d = c;
130 c = b;
131 b = a;
132 a = temp1 + temp2;
133 }
134
135 h[0] += a;
136 h[1] += b;
137 h[2] += c;
138 h[3] += d;
139 h[4] += e;
140 h[5] += f;
141 h[6] += g;
142 h[7] += hh;
143 }
144
145 std::ostringstream out;
146 out << std::hex << std::setfill('0');
147 for (uint32_t part : h) {
148 out << std::setw(8) << part;
149 }
150 return out.str();
151}
152
153std::string uppercase(std::string value) {
154 std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
155 return static_cast<char>(std::toupper(c));
156 });
157 return value;
158}
159
160std::string lowercase(std::string value) {
161 std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
162 return static_cast<char>(std::tolower(c));
163 });
164 return value;
165}
166
167std::string trim(const std::string& value) {
168 const auto begin = std::find_if_not(value.begin(), value.end(), [](unsigned char c) {
169 return std::isspace(c) != 0;
170 });
171 if (begin == value.end()) return {};
172
173 const auto end = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char c) {
174 return std::isspace(c) != 0;
175 }).base();
176
177 return std::string(begin, end);
178}
179
180std::string normalize_engine_id(const std::string& value) {
181 return lowercase(trim(value));
182}
183
184std::string compact_key(const std::string& raw) {
185 std::string out;
186 out.reserve(raw.size());
187 for (unsigned char c : raw) {
188 if (!std::isspace(c)) out.push_back(static_cast<char>(c));
189 }
190 return out;
191}
192
193std::string extract_activation_key(const std::string& raw) {
194 const std::string text = trim(raw);
195 if (text.empty()) return {};
196
197 const std::string begin_marker(kKeyBeginMarker);
198 const std::string end_marker(kKeyEndMarker);
199 const auto begin = text.find(begin_marker);
200 if (begin != std::string::npos) {
201 const auto end = text.find(end_marker, begin + begin_marker.size());
202 if (end != std::string::npos) {
203 return trim(text.substr(begin, end + end_marker.size() - begin));
204 }
205 return trim(text.substr(begin));
206 }
207
208 // BGRD2/BGRD3 can also be pasted without BEGIN/END markers. Keep the
209 // multiline field block intact because validation is calculated from
210 // field values, not from a compact one-line token.
211 if (((text.find(std::string(kKeyPrefixV2)) != std::string::npos &&
212 text.find("SIGNATURE=") != std::string::npos) ||
213 (text.find(std::string(kKeyPrefixV3)) != std::string::npos &&
214 text.find("CODE=") != std::string::npos))) {
215 return text;
216 }
217
218 // Legacy BGRD1 support. Paste operations from GUI toolkits can include
219 // invisible Unicode bytes or labels around the key. Keep only characters
220 // valid in the BGRD1 token and extract exactly one
221 // BGRD1.<payload>.<40-hex-signature> token.
222 std::string filtered;
223 filtered.reserve(raw.size());
224 for (unsigned char c : raw) {
225 if (std::isalnum(c) != 0 || c == '.' || c == '-' || c == '_') {
226 filtered.push_back(static_cast<char>(c));
227 }
228 }
229
230 const std::string prefix = std::string(kKeyPrefixV1) + ".";
231 const auto start = filtered.find(prefix);
232 if (start == std::string::npos) {
233 return compact_key(raw);
234 }
235
236 const auto payload_dot = filtered.find('.', start + prefix.size());
237 if (payload_dot == std::string::npos) {
238 return filtered.substr(start);
239 }
240
241 const size_t sig_start = payload_dot + 1;
242 size_t sig_end = sig_start;
243 while (sig_end < filtered.size() && sig_end - sig_start < 40 &&
244 std::isxdigit(static_cast<unsigned char>(filtered[sig_end])) != 0) {
245 ++sig_end;
246 }
247
248 if (sig_end - sig_start == 40) {
249 return filtered.substr(start, sig_end - start);
250 }
251
252 return filtered.substr(start);
253}
254
255std::string compact_hex_machine_id(const std::string& raw) {
256 std::string out;
257 out.reserve(raw.size());
258 for (unsigned char c : raw) {
259 if (std::isxdigit(c)) {
260 out.push_back(static_cast<char>(std::toupper(c)));
261 }
262 }
263 return out;
264}
265
266std::string collapse_spaces(const std::string& value) {
267 std::string out;
268 out.reserve(value.size());
269 bool last_space = false;
270
271 for (unsigned char c : value) {
272 if (c == '\0') continue;
273 if (std::isspace(c) != 0) {
274 if (!last_space && !out.empty()) {
275 out.push_back(' ');
276 }
277 last_space = true;
278 } else {
279 out.push_back(static_cast<char>(c));
280 last_space = false;
281 }
282 }
283
284 while (!out.empty() && out.back() == ' ') {
285 out.pop_back();
286 }
287 return out;
288}
289
290std::string canonical_serial(std::string value) {
291 value = trim(value);
292 value = collapse_spaces(value);
293 return uppercase(value);
294}
295
296bool all_zero_like(const std::string& value) {
297 bool has_alnum = false;
298 for (unsigned char c : value) {
299 if (std::isalnum(c) == 0) continue;
300 has_alnum = true;
301 if (c != '0') return false;
302 }
303 return has_alnum;
304}
305
306bool is_placeholder_serial(const std::string& value) {
307 const std::string serial = canonical_serial(value);
308 if (serial.empty()) return true;
309 if (all_zero_like(serial)) return true;
310
311 static constexpr std::string_view placeholders[] = {
312 "0",
313 "00",
314 "00000000",
315 "0000000000000000",
316 "00000000000000000000000000000000",
317 "NONE",
318 "UNKNOWN",
319 "UNKNOWN SERIAL",
320 "UNKNOWN SERIAL NUMBER",
321 "NOT SPECIFIED",
322 "NOT APPLICABLE",
323 "N/A",
324 "NA",
325 "DEFAULT STRING",
326 "SYSTEM SERIAL NUMBER",
327 "BASE BOARD SERIAL NUMBER",
328 "TO BE FILLED BY O.E.M.",
329 "TO BE FILLED BY OEM"
330 };
331
332 for (std::string_view placeholder : placeholders) {
333 if (serial == placeholder) return true;
334 }
335 return false;
336}
337
338std::string read_first_valid_serial(const std::vector<std::string>& paths) {
339 for (const auto& path : paths) {
340 std::ifstream input(path);
341 if (!input.is_open()) continue;
342 std::string line;
343 std::getline(input, line);
344 line = canonical_serial(line);
345 if (!is_placeholder_serial(line)) return line;
346 }
347 return {};
348}
349
350std::string read_first_existing_file(const std::vector<std::string>& paths);
351
353 std::string seed = read_first_existing_file({
354 "/etc/machine-id",
355 "/var/lib/dbus/machine-id"
356 });
357
358 if (seed.empty()) {
359 char hostname[256] = {};
360 if (gethostname(hostname, sizeof(hostname) - 1) == 0) {
361 seed = hostname;
362 }
363 }
364
365 if (seed.empty()) {
366 seed = "unknown-machine";
367 }
368
369 seed += "|";
370 seed += kProductId;
371
372 return uppercase(sha256_hex(seed)).substr(0, 16);
373}
374
375int b64url_value(char c) {
376 if (c >= 'A' && c <= 'Z') return c - 'A';
377 if (c >= 'a' && c <= 'z') return c - 'a' + 26;
378 if (c >= '0' && c <= '9') return c - '0' + 52;
379 if (c == '-' || c == '+') return 62;
380 if (c == '_' || c == '/') return 63;
381 return -1;
382}
383
384std::optional<std::string> base64url_decode(const std::string& input) {
385 std::string output;
386 int val = 0;
387 int valb = -8;
388
389 for (char c : input) {
390 if (c == '=') break;
391 const int decoded = b64url_value(c);
392 if (decoded < 0) return std::nullopt;
393 val = (val << 6) + decoded;
394 valb += 6;
395 if (valb >= 0) {
396 output.push_back(static_cast<char>((val >> valb) & 0xff));
397 valb -= 8;
398 }
399 }
400
401 return output;
402}
403
404std::string percent_decode(const std::string& value) {
405 std::string out;
406 out.reserve(value.size());
407
408 for (size_t i = 0; i < value.size(); ++i) {
409 if (value[i] == '%' && i + 2 < value.size()) {
410 const char h1 = value[i + 1];
411 const char h2 = value[i + 2];
412 if (std::isxdigit(static_cast<unsigned char>(h1)) &&
413 std::isxdigit(static_cast<unsigned char>(h2))) {
414 const std::string hex{h1, h2};
415 const char ch = static_cast<char>(std::strtol(hex.c_str(), nullptr, 16));
416 out.push_back(ch);
417 i += 2;
418 continue;
419 }
420 }
421 if (value[i] == '+') out.push_back(' ');
422 else out.push_back(value[i]);
423 }
424
425 return out;
426}
427
428std::map<std::string, std::string> parse_payload(const std::string& payload) {
429 std::map<std::string, std::string> fields;
430 std::stringstream ss(payload);
431 std::string item;
432
433 while (std::getline(ss, item, ';')) {
434 const auto eq = item.find('=');
435 if (eq == std::string::npos) continue;
436 const std::string key = item.substr(0, eq);
437 const std::string value = item.substr(eq + 1);
438 fields[key] = percent_decode(value);
439 }
440
441 return fields;
442}
443
444std::string read_first_existing_file(const std::vector<std::string>& paths) {
445 for (const auto& path : paths) {
446 std::ifstream input(path);
447 if (!input.is_open()) continue;
448 std::string line;
449 std::getline(input, line);
450 line = trim(line);
451 if (!line.empty()) return line;
452 }
453 return {};
454}
455
456std::string today_ymd() {
457 const std::time_t now = std::time(nullptr);
458 std::tm tm{};
459#if defined(_POSIX_VERSION)
460 localtime_r(&now, &tm);
461#else
462 tm = *std::localtime(&now);
463#endif
464 char buffer[16] = {};
465 std::strftime(buffer, sizeof(buffer), "%Y-%m-%d", &tm);
466 return buffer;
467}
468
469bool is_valid_expiry(const std::string& expires) {
470 if (expires == "never") return true;
471 if (expires.size() != 10) return false;
472 return std::isdigit(static_cast<unsigned char>(expires[0])) &&
473 std::isdigit(static_cast<unsigned char>(expires[1])) &&
474 std::isdigit(static_cast<unsigned char>(expires[2])) &&
475 std::isdigit(static_cast<unsigned char>(expires[3])) &&
476 expires[4] == '-' &&
477 std::isdigit(static_cast<unsigned char>(expires[5])) &&
478 std::isdigit(static_cast<unsigned char>(expires[6])) &&
479 expires[7] == '-' &&
480 std::isdigit(static_cast<unsigned char>(expires[8])) &&
481 std::isdigit(static_cast<unsigned char>(expires[9]));
482}
483
484bool has_expired(const std::string& expires) {
485 if (expires == "never") return false;
486 if (!is_valid_expiry(expires)) return true;
487 return expires < today_ymd();
488}
489
491 return ::geteuid() == 0;
492}
493
494std::string user_config_dir() {
495 if (const char* xdg = std::getenv("XDG_CONFIG_HOME"); xdg && *xdg) {
496 return (std::filesystem::path(xdg) / "bastionguard-recovery-data").string();
497 }
498 if (const char* home = std::getenv("HOME"); home && *home) {
499 return (std::filesystem::path(home) / ".config" / "bastionguard-recovery-data").string();
500 }
501 return ".";
502}
503
504std::string system_config_dir() {
505 return "/etc/bastionguard-recovery-data";
506}
507
509 return (std::filesystem::path(user_config_dir()) / "activation.key").string();
510}
511
513 return (std::filesystem::path(system_config_dir()) / "activation.key").string();
514}
515
517 // When the GUI is launched through pkexec/root, save a system-wide key so
518 // the activation is not tied to /root/.config and survives future root runs.
519 if (is_running_as_root()) {
521 }
523}
524
525std::string read_file_all(const std::string& path) {
526 std::ifstream input(path, std::ios::binary);
527 if (!input.is_open()) return {};
528 std::ostringstream content;
529 content << input.rdbuf();
530 return content.str();
531}
532
533std::string sign_payload_v1(const std::string& payload) {
534 const std::string input = std::string(kKeyPrefixV1) + "|" + payload + "|" + std::string(kActivationSecret);
535 return uppercase(sha256_hex(input)).substr(0, 40);
536}
537
538std::string sign_payload_v2(const std::string& canonical_payload) {
539 const std::string input = std::string(kKeyPrefixV2) + "\n" + canonical_payload + "\n" + std::string(kActivationSecret);
540 return uppercase(sha256_hex(input)).substr(0, 40);
541}
542
543uint64_t fnv1a64(const std::string& input) {
544 uint64_t hash = 14695981039346656037ULL;
545 for (unsigned char c : input) {
546 hash ^= static_cast<uint64_t>(c);
547 hash *= 1099511628211ULL;
548 }
549 return hash;
550}
551
552std::string normalize_activation_code(std::string value) {
553 value = uppercase(value);
554 std::string out;
555 out.reserve(value.size());
556 for (unsigned char c : value) {
557 if (std::isalnum(c) != 0) {
558 out.push_back(static_cast<char>(c));
559 }
560 }
561 return out;
562}
563
564std::string group_activation_code(const std::string& compact) {
565 std::string out;
566 for (size_t i = 0; i < compact.size(); ++i) {
567 if (i > 0 && i % 5 == 0) out.push_back('-');
568 out.push_back(compact[i]);
569 }
570 return out;
571}
572
573std::string field_or_empty(const std::map<std::string, std::string>& fields, const std::string& name) {
574 const auto it = fields.find(name);
575 return it == fields.end() ? std::string{} : it->second;
576}
577
578std::string code_payload_v3(const std::map<std::string, std::string>& fields) {
579 const std::string product = trim(field_or_empty(fields, "PRODUCT"));
580 const std::string engine = normalize_engine_id(field_or_empty(fields, "ENGINE"));
581 const std::string machine = compact_hex_machine_id(field_or_empty(fields, "MACHINE"));
582 const std::string motherboard = canonical_serial(field_or_empty(fields, "MOTHERBOARD"));
583 const std::string key_id = trim(field_or_empty(fields, "KEYID"));
584 std::string edition = trim(field_or_empty(fields, "EDITION"));
585 std::string expires = lowercase(trim(field_or_empty(fields, "EXPIRES")));
586
587 if (edition.empty()) edition = "Donation";
588 if (expires.empty()) expires = "never";
589
590 std::ostringstream out;
591 out << "product=" << product << "\n"
592 << "engine=" << engine << "\n"
593 << "machine=" << machine << "\n"
594 << "motherboard=" << motherboard << "\n"
595 << "edition=" << edition << "\n"
596 << "expires=" << expires;
597
598 // BGRD3.1: optional random KEYID. When present, it is part of the
599 // activation code calculation so the same hardware can receive different
600 // valid keys without making the readable BGRD3 block fragile.
601 if (!key_id.empty()) {
602 out << "\nkeyid=" << key_id;
603 }
604
605 return out.str();
606}
607
608std::string generate_activation_code_v3(const std::string& canonical_payload) {
609 static constexpr std::string_view alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
610 const std::string material = std::string(kKeyPrefixV3) + "\n" + canonical_payload + "\n" + std::string(kActivationSecret);
611
612 std::string compact;
613 compact.reserve(25);
614 for (int block = 0; block < 5; ++block) {
615 uint64_t h = fnv1a64(material + "\nblock=" + std::to_string(block));
616 for (int i = 0; i < 5; ++i) {
617 const uint64_t mixed = h ^ (h >> 29U) ^ (h >> 47U);
618 compact.push_back(alphabet[static_cast<size_t>(mixed & 31ULL)]);
619 h = (h >> 5U) ^ (h << 17U) ^ (0x9E3779B97F4A7C15ULL + static_cast<uint64_t>(block * 131 + i));
620 }
621 }
622
623 return group_activation_code(compact);
624}
625
626std::map<std::string, std::string> parse_key_block(const std::string& key) {
627 std::map<std::string, std::string> fields;
628 std::stringstream ss(key);
629 std::string line;
630
631 while (std::getline(ss, line)) {
632 line = trim(line);
633 if (line.empty()) continue;
634 if (line == kKeyBeginMarker || line == kKeyEndMarker) continue;
635 if (line == kKeyPrefixV2 || line == kKeyPrefixV3) {
636 fields["FORMAT"] = line;
637 continue;
638 }
639
640 const auto eq = line.find('=');
641 if (eq == std::string::npos) continue;
642 std::string name = uppercase(trim(line.substr(0, eq)));
643 std::string value = trim(line.substr(eq + 1));
644 fields[name] = value;
645 }
646
647 return fields;
648}
649
650std::string canonical_v2_payload(const std::map<std::string, std::string>& fields) {
651 const std::string product = trim(field_or_empty(fields, "PRODUCT"));
652 const std::string engine = normalize_engine_id(field_or_empty(fields, "ENGINE"));
653 const std::string machine = compact_hex_machine_id(field_or_empty(fields, "MACHINE"));
654 const std::string motherboard = canonical_serial(field_or_empty(fields, "MOTHERBOARD"));
655 const std::string owner = trim(field_or_empty(fields, "OWNER"));
656 std::string edition = trim(field_or_empty(fields, "EDITION"));
657 std::string expires = lowercase(trim(field_or_empty(fields, "EXPIRES")));
658
659 if (edition.empty()) edition = "Donation";
660 if (expires.empty()) expires = "never";
661
662 std::ostringstream out;
663 out << "product=" << product << "\n"
664 << "engine=" << engine << "\n"
665 << "machine=" << machine << "\n"
666 << "motherboard=" << motherboard << "\n"
667 << "owner=" << owner << "\n"
668 << "edition=" << edition << "\n"
669 << "expires=" << expires;
670 return out.str();
671}
672
673} // namespace
674
675std::string activation_engine_id() {
676 return std::string(kActivationEngineId);
677}
678
679std::string machine_id() {
680 const std::string raw_machine_id = read_first_existing_file({
681 "/etc/machine-id",
682 "/var/lib/dbus/machine-id"
683 });
684
685 const std::string cleaned = compact_hex_machine_id(raw_machine_id);
686 if (cleaned.size() == 32) {
687 // Use the real Linux machine-id exactly as requested: this is the
688 // identifier shown in About → Activation and stored inside keys.
689 return cleaned;
690 }
691
692 // Fallback only for unusual systems where /etc/machine-id is unavailable
693 // or malformed. This keeps activation usable without changing the public UI.
694 return legacy_application_machine_id();
695}
696
697std::string motherboard_serial() {
698 const std::string serial = read_first_valid_serial({
699 "/sys/class/dmi/id/board_serial",
700 "/sys/devices/virtual/dmi/id/board_serial",
701 "/sys/class/dmi/id/product_serial",
702 "/sys/devices/virtual/dmi/id/product_serial",
703 "/sys/class/dmi/id/product_uuid",
704 "/sys/devices/virtual/dmi/id/product_uuid"
705 });
706
707 return serial;
708}
709
710std::string activation_key_path() {
711 const std::string system_path = system_activation_key_path();
712 std::error_code ec;
713 if (std::filesystem::exists(system_path, ec) && !ec) {
714 return system_path;
715 }
716
717 if (is_running_as_root()) {
718 return system_path;
719 }
720
721 return user_activation_key_path();
722}
723
724ActivationStatus validate_key(const std::string& raw_key) {
725 ActivationStatus status;
726 status.machine_id = machine_id();
728
729 const std::string key = extract_activation_key(raw_key);
730 if (key.empty()) {
731 status.message = "No activation key installed.";
732 return status;
733 }
734
735 const auto apply_common_field_checks = [&](const std::string& product,
736 const std::string& key_machine_raw,
737 const std::string& key_motherboard_raw,
738 const std::string& owner,
739 const std::string& edition,
740 const std::string& expires) -> bool {
741 if (product != kProductId) {
742 status.message = "Activation key is for a different product.";
743 return false;
744 }
745
746 if (key_machine_raw.empty()) {
747 status.message = "Activation key does not contain a Machine ID.";
748 return false;
749 }
750
751 if (key_motherboard_raw.empty()) {
752 status.message = "Activation key does not contain a motherboard serial.";
753 return false;
754 }
755
756 if (status.motherboard_serial.empty()) {
757 status.message = "Motherboard serial could not be read on this computer.";
758 return false;
759 }
760
761 const std::string key_machine = compact_hex_machine_id(key_machine_raw);
762 const std::string current_machine = compact_hex_machine_id(status.machine_id);
763 const std::string legacy_machine = compact_hex_machine_id(legacy_application_machine_id());
764
765 if (key_machine != current_machine && key_machine != legacy_machine) {
766 status.message = "Activation key is for a different machine-id.";
767 return false;
768 }
769
770 const std::string key_motherboard = canonical_serial(key_motherboard_raw);
771 const std::string current_motherboard = canonical_serial(status.motherboard_serial);
772
773 if (key_motherboard != current_motherboard) {
774 status.message = "Activation key is for a different motherboard serial.";
775 return false;
776 }
777
778 status.owner = owner.empty() ? "Donation supporter" : owner;
779 status.edition = edition.empty() ? "Donation" : edition;
780 status.expires = expires.empty() ? "never" : lowercase(expires);
781
782 if (!is_valid_expiry(status.expires)) {
783 status.expired = true;
784 status.message = "Activation key contains an invalid expiry date.";
785 return false;
786 }
787
788 if (has_expired(status.expires)) {
789 status.expired = true;
790 status.message = "Activation key has expired.";
791 return false;
792 }
793
794 status.active = true;
795 status.message = "Activation is valid.";
796 return true;
797 };
798
799 // Preferred clean key format: BGRD3. This avoids SHA-based signing and uses
800 // a simple owner-only donation code calculated from product, engine,
801 // /etc/machine-id, motherboard serial, edition, expiry, and the private
802 // owner secret. The visible owner name is informational and does not affect
803 // the code, which avoids UTF-8/locale copy-paste mismatches.
804 if (key.find(std::string(kKeyPrefixV3)) != std::string::npos) {
805 const auto fields = parse_key_block(key);
806 const std::string code = normalize_activation_code(field_or_empty(fields, "CODE"));
807 const std::string product = trim(field_or_empty(fields, "PRODUCT"));
808 const std::string engine = normalize_engine_id(field_or_empty(fields, "ENGINE"));
809 const std::string machine = compact_hex_machine_id(field_or_empty(fields, "MACHINE"));
810 const std::string motherboard = canonical_serial(field_or_empty(fields, "MOTHERBOARD"));
811 const std::string owner = trim(field_or_empty(fields, "OWNER"));
812 const std::string edition = trim(field_or_empty(fields, "EDITION"));
813 const std::string expires = lowercase(trim(field_or_empty(fields, "EXPIRES")));
814
815 if (field_or_empty(fields, "FORMAT") != std::string(kKeyPrefixV3)) {
816 status.message = "Invalid BGRD3 activation key: BGRD3 format line is missing.";
817 return status;
818 }
819
820 if (code.size() != 25) {
821 status.message = "Invalid BGRD3 activation key: CODE must contain 25 letters/numbers.";
822 return status;
823 }
824
825 if (engine.empty()) {
826 status.message = "Invalid BGRD3 activation key: ENGINE field is missing.";
827 return status;
828 }
829
830 const std::string current_engine = normalize_engine_id(activation_engine_id());
831 if (normalize_engine_id(engine) != current_engine) {
832 status.message = "Activation engine mismatch. App engine is " + current_engine +
833 " but key engine is " + engine +
834 ". Rebuild and reinstall the app and generate the keygen from the same package.";
835 return status;
836 }
837
838 const std::string canonical_payload = code_payload_v3(fields);
839 const std::string expected_code = normalize_activation_code(generate_activation_code_v3(canonical_payload));
840 if (code != expected_code) {
841 status.message = "BGRD3 activation code is not valid. The key was not saved. Generate the key again with the matching keygen and copy the whole BEGIN/END block.";
842 return status;
843 }
844
845 apply_common_field_checks(product, machine, motherboard, owner, edition, expires);
846 return status;
847 }
848
849 // Legacy clean key format: multiline BGRD2 block. The key explicitly
850 // carries the activation engine so mismatched app/keygen builds are
851 // diagnosed before signature validation.
852 if (key.find(std::string(kKeyPrefixV2)) != std::string::npos) {
853 const auto fields = parse_key_block(key);
854 const std::string signature = uppercase(trim(field_or_empty(fields, "SIGNATURE")));
855 const std::string product = trim(field_or_empty(fields, "PRODUCT"));
856 const std::string engine = normalize_engine_id(field_or_empty(fields, "ENGINE"));
857 const std::string machine = compact_hex_machine_id(field_or_empty(fields, "MACHINE"));
858 const std::string motherboard = canonical_serial(field_or_empty(fields, "MOTHERBOARD"));
859 const std::string owner = trim(field_or_empty(fields, "OWNER"));
860 const std::string edition = trim(field_or_empty(fields, "EDITION"));
861 const std::string expires = lowercase(trim(field_or_empty(fields, "EXPIRES")));
862
863 if (signature.size() != 40) {
864 status.message = "Invalid BGRD2 activation key: SIGNATURE must contain 40 hexadecimal characters.";
865 return status;
866 }
867 for (unsigned char c : signature) {
868 if (std::isxdigit(c) == 0) {
869 status.message = "Invalid BGRD2 activation key: SIGNATURE contains a non-hexadecimal character.";
870 return status;
871 }
872 }
873
874 if (engine.empty()) {
875 status.message = "Invalid BGRD2 activation key: ENGINE field is missing.";
876 return status;
877 }
878
879 const std::string current_engine = normalize_engine_id(activation_engine_id());
880 if (normalize_engine_id(engine) != current_engine) {
881 status.message = "Activation engine mismatch. App engine is " + current_engine +
882 " but key engine is " + engine +
883 ". Rebuild and reinstall the app and generate the keygen from the same package.";
884 return status;
885 }
886
887 const std::string canonical_payload = canonical_v2_payload(fields);
888 const std::string expected_signature = sign_payload_v2(canonical_payload);
889 if (signature != expected_signature) {
890 status.message = "BGRD2 activation key signature is not valid. The key was not saved. Generate the key again with the matching keygen and copy the whole BEGIN/END block.";
891 return status;
892 }
893
894 apply_common_field_checks(product, machine, motherboard, owner, edition, expires);
895 return status;
896 }
897
898 // Legacy BGRD1 one-line key support.
899 const std::string prefix = std::string(kKeyPrefixV1) + ".";
900 if (key.rfind(prefix, 0) != 0) {
901 status.message = "Invalid activation key prefix. Paste a BGRD3 activation block generated by the matching keygen.";
902 return status;
903 }
904
905 const auto first_dot = key.find('.', prefix.size());
906 if (first_dot == std::string::npos) {
907 status.message = "Malformed legacy activation key.";
908 return status;
909 }
910
911 const std::string payload_b64 = key.substr(prefix.size(), first_dot - prefix.size());
912 const std::string signature = uppercase(key.substr(first_dot + 1));
913
914 if (signature.size() != 40) {
915 status.message = "Invalid legacy activation key signature length.";
916 return status;
917 }
918
919 const auto decoded_payload = base64url_decode(payload_b64);
920 if (!decoded_payload.has_value()) {
921 status.message = "Legacy activation key payload is not valid base64url.";
922 return status;
923 }
924
925 const std::string expected_signature = sign_payload_v1(*decoded_payload);
926 if (signature != expected_signature) {
927 status.message = "Legacy activation key signature is not valid. Generate a new BGRD3 key with the matching keygen.";
928 return status;
929 }
930
931 const auto fields = parse_payload(*decoded_payload);
932 const auto product_it = fields.find("product");
933 const auto machine_it = fields.find("machine");
934 const auto motherboard_it = fields.find("motherboard");
935 const auto owner_it = fields.find("owner");
936 const auto edition_it = fields.find("edition");
937 const auto expires_it = fields.find("expires");
938
939 apply_common_field_checks(
940 product_it != fields.end() ? product_it->second : std::string{},
941 machine_it != fields.end() ? machine_it->second : std::string{},
942 motherboard_it != fields.end() ? motherboard_it->second : std::string{},
943 owner_it != fields.end() ? owner_it->second : std::string{},
944 edition_it != fields.end() ? edition_it->second : std::string{},
945 expires_it != fields.end() ? expires_it->second : std::string{}
946 );
947 return status;
948}
949
951 const std::string system_path = system_activation_key_path();
952 const std::string user_path = user_activation_key_path();
953
954 std::error_code ec;
955 if (std::filesystem::exists(system_path, ec) && !ec) {
956 ActivationStatus system_status = validate_key(read_file_all(system_path));
957 if (system_status.active) {
958 return system_status;
959 }
960
961 if (std::filesystem::exists(user_path, ec) && !ec) {
962 ActivationStatus user_status = validate_key(read_file_all(user_path));
963 if (user_status.active) {
964 return user_status;
965 }
966 }
967
968 return system_status;
969 }
970
971 return validate_key(read_file_all(user_path));
972}
973
974bool save_key_if_valid(const std::string& raw_key, std::string& error_message) {
975 const ActivationStatus status = validate_key(raw_key);
976 if (!status.active) {
977 error_message = status.message;
978 return false;
979 }
980
981 const std::string target_path = preferred_activation_key_path_for_save();
982 const std::string dir = std::filesystem::path(target_path).parent_path().string();
983
984 std::error_code ec;
985 std::filesystem::create_directories(dir, ec);
986 if (ec) {
987 error_message = "Cannot create activation directory '" + dir + "': " + ec.message();
988 return false;
989 }
990
991 std::ofstream output(target_path, std::ios::binary | std::ios::trunc);
992 if (!output.is_open()) {
993 error_message = "Cannot write activation key file: " + target_path;
994 if (!is_running_as_root() && target_path.rfind("/etc/", 0) == 0) {
995 error_message += " Run the application as root/pkexec.";
996 }
997 return false;
998 }
999
1000 output << extract_activation_key(raw_key) << "\n";
1001 output.close();
1002
1003 if (!output) {
1004 error_message = "Activation key file was opened but could not be written completely: " + target_path;
1005 return false;
1006 }
1007
1008 std::error_code verify_ec;
1009 if (!std::filesystem::exists(target_path, verify_ec) || verify_ec) {
1010 error_message = "Activation key was not created at: " + target_path;
1011 if (verify_ec) error_message += " (" + verify_ec.message() + ")";
1012 return false;
1013 }
1014
1015 const ActivationStatus saved_status = validate_key(read_file_all(target_path));
1016 if (!saved_status.active) {
1017 error_message = "Activation key was written but could not be read back as valid: " + saved_status.message;
1018 return false;
1019 }
1020
1021 error_message.clear();
1022 return true;
1023}
1024
1025} // namespace BastionGuard::Activation
std::string extract_activation_key(const std::string &raw)
std::string code_payload_v3(const std::map< std::string, std::string > &fields)
std::map< std::string, std::string > parse_key_block(const std::string &key)
std::map< std::string, std::string > parse_payload(const std::string &payload)
std::string generate_activation_code_v3(const std::string &canonical_payload)
std::string canonical_v2_payload(const std::map< std::string, std::string > &fields)
std::string normalize_engine_id(const std::string &value)
std::string group_activation_code(const std::string &compact)
std::optional< std::string > base64url_decode(const std::string &input)
std::string read_first_valid_serial(const std::vector< std::string > &paths)
std::string sign_payload_v2(const std::string &canonical_payload)
std::string collapse_spaces(const std::string &value)
std::string read_first_existing_file(const std::vector< std::string > &paths)
uint32_t rotr(uint32_t value, unsigned int count)
std::string field_or_empty(const std::map< std::string, std::string > &fields, const std::string &name)
std::string sign_payload_v1(const std::string &payload)
std::string compact_hex_machine_id(const std::string &raw)
ActivationStatus validate_key(const std::string &raw_key)
std::string motherboard_serial()
ActivationStatus validate_installed_key()
std::string activation_key_path()
bool save_key_if_valid(const std::string &raw_key, std::string &error_message)
std::string activation_engine_id()