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:

btnOKClick.png

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 successfully
  • 0 : 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:

FieldBit CountFunction UsedDescription
Delivery1 bitDecodeByteDelivery method flag
Product ID5 bitsDecodeByteProduct edition identifier
Expiration6 bitsDecodeLongWordLicense expiration date
Serial23 bitsDecodeLongWordUnique serial number
Checksum24 bitsDecodeLongWordData 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

  1. Input validation: Verify product_id against known valid values
  2. String construction: Concatenate serial, XOR(serial), and expiration
  3. Initial state: Set checksum base value to ~serial & MAX_SERIAL
  4. 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:

  1. Implement bit-to-Base32 encoding function (reverse of digit_to_bits)
  2. Create license parameter generator with valid constraints
  3. Build complete key generation pipeline
  4. 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: activated