Overview
License information structure
The TRMRegInfo
struct stores the decoded license information. Based on memory access patterns in the Decode
function, the structure layout is:
struct TRMRegInfo {
wchar_t *Key; // +0x08 Input key string (format: RM10-XXXX-XXXX-XXXX)
uint8_t Delivery; // +0x10 Delivery method flag
uint8_t ProductIndex; // +0x11 Index into ProductIDs[] array
uint64_t Serial; // +0x18 Unique serial number
double Expiration; // +0x20 Expiration date (TDateTime format)
};
License key format analysis
Key structure
RootsMagic license keys follow the pattern: RM10-XXXX-XXXX-XXXX
- Prefix:
RM10-
(constant identifier) - Payload: 12 characters using custom Base32 encoding
- Total length: 17 characters including hyphens
60-Bit buffer architecture
Provides a comprehensive analysis of the RootsMagic license key validation system, including the decoding process, data structures, and validation algorithms.
Key components
1. Registration dialog handler Registration::TfrmRegistration::btnOKClick()
This function serves as the event handler for the OK button in the registration dialog box and performs the following operations:
- Validates the user-entered name and license key
- Decodes and verifies the license information
- On successful validation:
- Saves the license data to preferences
- Opens the
TfrmRegisterOnline
window
- On failure:
- Displays an appropriate error message
The operation follows a straightforward validation pipeline as illustrated in the pseudo-code below:
2. License decoding engine Rmregistration::TRMRegInfo::Decode(void)::DigitToBits()
char __fastcall Rmregistration::TRMRegInfo::Decode(Rmregistration::TRMRegInfo *this, double a2)
{
// variables declarations cleaned for clarity
_QWORD *SerialBitsValue;
char DeliveryBitValue;
_BYTE v18[64];
__int64 computedChecksum;
unsigned __int8 ExpirationBitsValue;
char ProductIDBitsValue;
Rmregistration::TRMRegInfo *v35;
v16 = 0;
v15 = 0;
v14 = 0;
v17 = 0;
v26 = 0;
v35 = this;
v34 = 0;
System::Sysutils::TStringHelper::Trim((System::Sysutils::TStringHelper *)&v17);
System::_UStrAsg(this, v17);
*((_BYTE *)v35 + 16) = 0;
*((_BYTE *)v35 + 17) = 0;
*((_QWORD *)v35 + 4) = 0;
*((_QWORD *)v35 + 3) = 0;
for ( i = 0; i != 60; ++i )
v18[i] = 0;
v23 = (char *)v35 + 8;
if ( (unsigned __int8)System::Sysutils::TStringHelper::StartsWith((char *)v35 + 8, Rmregistration::KeyPrefix, 0) )
// Check that the license begins with “RM10”.
{
v32 = 0;
v22 = 0;
if ( Rmregistration::KeyPrefix )
v22 = *((_DWORD *)Rmregistration::KeyPrefix - 1);
v21 = (_QWORD *)((char *)v35 + 8);
v20 = 0;
if ( *((_QWORD *)v35 + 1) )
v20 = *(_DWORD *)(*v21 - 4LL);
i = v22;
if ( v22 <= v20 - 1 )
{
v24 = v20;
do
{
v13 = System::_UniqueStringU((char *)v35 + 8);
v31 = System::Character::TCharHelper::ToUpper((System::Character::TCharHelper *)(v13 + 2LL * i));
if ( v31 != 32 && v31 != 45 )
{
if ( (unsigned __int16)(v31 - 48) >= 0xAu && (unsigned __int16)(v31 - 65) >= 0x1Au )
goto LABEL_35;
Rmregistration::TRMRegInfo::Decode(void)::DigitToBits(v18, (unsigned int)(5 * v32++), v31);
// Decode a single Base32 character to 5 bits
}
++i;
}
while ( i != v24 );
}
if ( v32 == 12 ) // You need 12 valid characters (numbers or letters) to parse (excluding hyphens/spaces).
{
DeliveryBitValue = Rmregistration::TRMRegInfo::Decode(void)::DecodeByte(v18, &Rmregistration::DeliveryBit, 0);
*((_BYTE *)v35 + 17) = DeliveryBitValue == 1;
ProductIDBitsValue = Rmregistration::TRMRegInfo::Decode(void)::DecodeByte(v18, &Rmregistration::ProductIDBits, 4);
v25 = 0;
while ( ProductIDBitsValue != Rmregistration::ProductIDs[v25] )
{
if ( ++v25 == 5 )
goto LABEL_23;
}
*((_BYTE *)v35 + 16) = v25;
LABEL_23:
ExpirationBitsValue = Rmregistration::TRMRegInfo::Decode(void)::DecodeLongWord(
v18,
&Rmregistration::ExpirationBits,
5);
if ( ExpirationBitsValue )
{
v11 = (double *)((char *)v35 + 32);
a2 = Rmregistration::TRMRegInfo::Decode(void)::IncMonths(v18, ExpirationBitsValue, Rmregistration::MinExpDate);
*v11 = a2;
}
SerialBitsValue = (_QWORD *)((char *)v35 + 24);
*SerialBitsValue = Rmregistration::TRMRegInfo::Decode(void)::DecodeLongWord(v18, &Rmregistration::SerialBits, 23);
computedChecksum = Rmregistration::MaxSerial & ~*((_QWORD *)v35 + 3);
System::Sysutils::IntToStr((System::Sysutils *)&v16, *((_QWORD *)v35 + 3));
v9 = v16;
System::Sysutils::IntToStr((System::Sysutils *)&v15, computedChecksum);
v8 = v15;
System::Sysutils::IntToStr((System::Sysutils *)&v14, ExpirationBitsValue);
System::_UStrCatN((unsigned int)&v26, 3, v9, v8, v14, v2);
v19 = 0;
if ( v26 )
v19 = *(_DWORD *)(v26 - 4);
i = 1;
if ( v19 >= 1 )
{
v24 = v19 + 1;
do
{
v7 = System::_UniqueStringU(&v26);
v3 = System::Character::TCharHelper::ToUpper((System::Character::TCharHelper *)(v7 + 2LL * (i - 1)));
v27 = Rmregistration::MaxSerial
& ((Rmregistration::ProductIDs[*((unsigned __int8 *)v35 + 16)] + i + v3) << (i & 0x1F));
computedChecksum = Rmregistration::MaxSerial & (v27 ^ computedChecksum);
++i;
}
while ( i != v24 );
}
v6 = computedChecksum;
if ( v6 == Rmregistration::TRMRegInfo::Decode(void)::DecodeLongWord(v18, &Rmregistration::ChecksumBits, 23)
&& (*((double *)v35 + 4) <= 0.0
|| (v5 = *((double *)v35 + 4), System::Sysutils::Now((System::Sysutils *)v18), a2 <= v5)) )
{
v34 = 1;
}
else
{
*((_BYTE *)v35 + 16) = 0;
}
}
}
LABEL_35:
System::_UStrClr(&v16);
System::_UStrClr(&v15);
System::_UStrClr(&v14);
System::_UStrClr(&v17);
System::_UStrClr(&v26);
return v34;
}
This core function handles the license key decoding process with the following responsibilities:
Input processing:
- Accepts a license key string with the known prefix format (KeyPrefix)
- Performs bit-by-bit decoding and validation
Validation checks:
- Format validation: Ensures proper key structure
- Product verification: Validates product ID against known values
- Expiration check: Verifies license validity period
- Checksum verification: Validates data integrity using proprietary algorithm
- License type: Determines if license is temporary or permanent (≤ 0.0)
Return values:
1
: All validations passed successfully0
: One or more validations failed
Data structures
This struct is what the license information gets decoded into. Based on how it’s used in Decode, we can infer its structure from accesses like:
struct TRMRegInfo {
wchar_t *Key; // +0x08 input key string (RM10-XXXX-XXXX-XXXX)
uint8_t Delivery; // +0x10 decoded Delivery flag
uint8_t ProductIndex; // +0x11 decoded ProductIDs[] index
uint64_t Serial; // +0x18 decoded serial number
double Expiration; // +0x20 decoded expiration date (TDateTime)
};
Structure of the 60-Bit Buffer (v18)
The 12-character payload is converted into a 60-bit buffer using the DigitToBits
function:
_BYTE v18[60]; // Bit buffer where each entry is 0 or 1
Conversion logic: Each Base32 character → 5 bits → 12 × 5 = 60 total bits
Base32 decoding implementation
Custom base32 character mapping
RootsMagic uses a proprietary Base32 encoding scheme with the following character-to-value mapping:
# Custom Base32 character mapping (indexed by ASCII offset from 48)
# Index = ord(char) - 48
BASE32_VALUES = [
0x1F, 0x1B, 0x1D, 0x01, 0x10, 0x03, 0x08, 0x05, 0x1E, 0x00, # '0'-'9'
*([0xFF] * 7), # Undefined values for ASCII 58–64
0x0D, 0x1E, 0x06, 0x1C, 0x0F, 0x07, 0x02, 0x0B, 0x1B, 0x0E, # 'A'-'J'
0x13, 0x09, 0x0A, 0x1A, 0x1F, 0x11, 0x15, 0x14, 0x03, 0x17, # 'K'-'T'
0x19, 0x0C, 0x16, 0x12, 0x04, 0x18, *([0xFF] * 5) # 'U'-'Z' + padding
]
def digit_to_bits(bit_array, offset, char):
"""
Decode a single Base32 character to 5 bits and write to bit_array[offset:].
Args:
bit_array: Target bit array to write to
offset: Starting position in the bit array
char: Base32 character to decode
"""
val = ord(char.upper())
index = val - 48
# Validate character range
if index < 0 or index >= len(BASE32_VALUES):
return 0 # Invalid character
base32_val = BASE32_VALUES[index]
if base32_val > 0x1F: # Values > 31 indicate invalid mapping
return 0 # Invalid value (mapped to 0xFF)
# Extract 5 bits and write to array
for i in range(5):
if offset + i <= 59:
bit_array[offset + i] = (base32_val >> (4 - i)) & 1
return 5
Example usage :
# Example: Decode a 12-character key segment
key_payload = "CZG0RW8YP6YD" # 12 valid Base32 characters
bit_array = [0] * 60
offset = 0
for ch in key_payload:
written = digit_to_bits(bit_array, offset, ch)
if written == 0:
print(f"Error: Invalid character in key: {ch}")
break
offset += written
# Display the resulting 60-bit array
print("Decoded bit array:", bit_array)
Data extraction from Bit Array
Bit position mapping
The 60-bit array contains license information encoded at specific bit positions:
Field | Bit Count | Function Used | Description |
---|---|---|---|
Delivery | 1 bit | DecodeByte | Delivery method flag |
Product ID | 5 bits | DecodeByte | Product edition identifier |
Expiration | 6 bits | DecodeLongWord | License expiration date |
Serial | 23 bits | DecodeLongWord | Unique serial number |
Checksum | 24 bits | DecodeLongWord | Data integrity verification |
Bit extraction algorithm
Both DecodeByte
and DecodeLongWord
functions use the same underlying bit extraction mechanism:
__int64 __fastcall Rmregistration::TRMRegInfo::Decode(void)::DecodeFunc(__int64 a1, __int64 a2, int a3)
{
int v4;
__int64 v5;
v5 = 0;
v4 = 0;
if ( a3 >= 0 )
{
do
{
if ( *(_BYTE *)(a1 + *(unsigned __int8 *)(a2 + v4)) )
v5 += (unsigned int)(1 << ((a3 - v4) & 0x1F));
++v4;
}
while ( v4 != a3 + 1 );
}
return v5;
}
Which likely translate to:
def bits_to_val(bits, positions):
"""
Extract a value from specific bit positions in the bit array.
Args:
bits: The 60-bit array
positions: List of bit indices to extract (MSB first)
Returns: Integer value reconstructed from the specified bits
"""
return sum((bits[p] & 1) << (len(positions) - 1 - i) for i, p in enumerate(positions))
Complete extraction example
# Example: Extract all fields from decoded key "CZG0RW8YP6YD"
bit_array = [0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1,
1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0,
1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0]
# Bit position mappings for each field
DeliveryBit = [38]
ProductIDBits = [23, 15, 46, 44, 19]
ExpirationBits = [24, 45, 52, 13, 42, 17]
SerialBits = [
49, 4, 56, 11, 22, 28, 57, 35, 27, 51, 1,
59, 36, 7, 31, 20, 26, 18, 14, 53, 30, 5, 32
]
ChecksumBits = [
39, 8, 12, 16, 47, 37, 54, 43, 34, 41, 21,
0, 29, 25, 50, 6, 55, 10, 58, 9, 3, 48,
33, 2
]
# Product edition mappings
ProductNames = [
"Trial Version", # ID: 0x14 (20)
"Standard Edition", # ID: 0x1B (27)
"UK Edition", # ID: 0x08 (8)
"FHC Edition", # ID: 0x0F (15)
"Public Preview" # ID: 0x12 (18)
]
# Extract values from bit array
delivery = bits_to_val(bit_array, DeliveryBit)
product_id = bits_to_val(bit_array, ProductIDBits)
expiration = bits_to_val(bit_array, ExpirationBits)
serial = bits_to_val(bit_array, SerialBits)
checksum = bits_to_val(bit_array, ChecksumBits)
print(f"Delivery: {delivery}") # 0
print(f"Product ID: {product_id}") # 15
print(f"Expiration: {expiration}") # 13
print(f"Serial: {serial}") # 1524135
print(f"Checksum: {checksum}") # 1312139
Checksum validation algorithm
The checksum serves as the primary integrity verification mechanism for the license key. The algorithm combines multiple inputs to generate a unique validation hash.
Implementation
def calculate_checksum(serial, expiration, product_id):
"""
Calculate the proprietary checksum for license validation.
Args:
serial: License serial number (23-bit value)
expiration: License expiration date
product_id: Product edition identifier
"""
# Valid product IDs (hexadecimal values)
VALID_PRODUCT_IDS = [0x14, 0x1B, 0x08, 0x0F, 0x12] # [20, 27, 8, 15, 18]
if product_id not in VALID_PRODUCT_IDS:
print(f"Error: Invalid product_id ({product_id}). Valid IDs: {VALID_PRODUCT_IDS}")
return False
MAX_SERIAL = 0xFFFFFF # 24-bit mask (16,777,215)
# Step 1: Construct input string from components
s1 = str(serial) # Serial as string
s2 = str(MAX_SERIAL ^ serial) # XOR complement of serial
s3 = str(expiration) # Expiration as string
key_string = s1 + s2 + s3 # Concatenated input
# Step 2: Initialize checksum with inverted serial
checksum = MAX_SERIAL & (~serial)
# Step 3: Process each character of the input string
for i, char in enumerate(key_string):
ascii_val = ord(char.upper())
# Generate position-dependent shift value
shift_amount = (product_id + i + ascii_val) << (i & 0x1F)
# Update checksum with XOR operation
checksum = MAX_SERIAL & (checksum ^ shift_amount)
return checksum
# Example usage
calculated_checksum = calculate_checksum(serial, expiration, product_id)
print(f"Calculated checksum: {calculated_checksum}") # 8648718
Algorithm breakdown
- Input validation: Verify product_id against known valid values
- String construction: Concatenate serial, XOR(serial), and expiration
- Initial state: Set checksum base value to ~serial & MAX_SERIAL
- Character processing: For each character in the constructed string:
- Convert to uppercase ASCII value
- Generate position-dependent shift using product_id, character index, and ASCII value
- Apply bit shift based on character position
- XOR with current checksum value
- Mask result to 24-bit range
Key generation process
Now that we understand the decoding and validation process, the next steps involve:
- Generate valid license parameters (serial, expiration, product_id)
- Calculate the corresponding checksum using the algorithm above
- Encode all fields back into the 60-bit array
- Convert bit array to Base32 characters
- Format as
RM10-XXXX-XXXX-XXXX
pattern
2. Implementation notes
Security considerations:
- No username binding: The license key is not tied to any specific username
- No online verification: All validation is performed locally
- Key sharing: Multiple users can theoretically use the same valid key
Next development steps:
- Implement bit-to-Base32 encoding function (reverse of
digit_to_bits
) - Create license parameter generator with valid constraints
- Build complete key generation pipeline
- Test generated keys against the original validation system
Activating the app
I managed to activate the app in the end (I just had to hook a function to return 1), but that’s not the focus of this article: