Minter — Technical Processes & Explanations
Architecture and implementation detail for the Minter licensing system. Companion to the Minter User Guide. Not linked from any menu.
Architecture Overview
Minter is split into two components deliberately kept separate:
- Minter.xlsm (Excel + VBA) — the user interface, orchestration, and data storage layer
- Signer.exe (C#, .NET, NSec.Cryptography) — the only component that touches private key material or performs Ed25519 signing
VBA never implements cryptography itself and never receives a private key value. It communicates with Signer.exe over stdin/stdout JSON — never via command-line arguments, so no sensitive value is ever visible in a process list or command-line log.
The Mint Sequence, Exactly
Every mint follows a fixed order. LicenceLog.txt is the sole source of truth for licence data — the Excel table is always a refreshed view derived from it, never written to independently.
- Sign — Signer.exe produces the signed licence string. Nothing is persisted yet.
- Write LicenceLog.txt — the licence becomes real and committed at this point.
- Write LicenceHistory.txt — append-only MINT audit entry.
- Refresh Excel — the on-screen table is reloaded from LicenceLog.txt.
- Create PDF — certificate generated; PDF_CREATED audit entry appended.
| Failure point | Treated as |
|---|---|
| Signing fails | Nothing recorded — full stop |
| LicenceLog.txt write fails | Nothing recorded — signature discarded |
| History write fails | Licence valid; audit gap flagged narrowly |
| Excel refresh fails | Licence valid; use Rebuild From Audit File |
| PDF generation fails | Licence valid; use Regenerate PDF |
Key Lifecycle & Rotation Math
Each product (Trust Armory, Support Funnel) has an independent key history. Exactly one key per product may be Active at a time. Generating a key never auto-activates it; restoring from an encrypted backup never auto-activates it either — activation is always a separate, explicit action.
Safe-to-retire calculation for an old key in the plugin’s PUBLIC_KEYS verification array:
safe_to_remove_date = MAX(ExpiryDate) across all LicenceLog rows for that KeyVersion, + applicable grace period
- A Perpetual licence under a given key means that key can never be safely removed — no future date resolves this
- Grace periods (trial grace, domain-change grace) extend the effective expiry beyond the stored ExpiryDate value
- Practical guidance: keep 2–3 keys in rotation for manageable housekeeping, though nothing enforces this as a hard limit
Encrypted Key Backup / Restore — Technical Detail
Two distinct encryption mechanisms exist in this system, and they are not interchangeable:
| keystore.dat | KeyBackups\*.enc |
|---|---|
| Windows DPAPI (CryptProtectData), CurrentUser scope | AES-256-GCM, key derived via PBKDF2-HMAC-SHA256 (600,000 iterations) from a user-chosen password |
| Bound to this specific Windows account + machine | Portable — works on any machine, given the password |
| Not a backup mechanism | The only supported migration/disaster-recovery path |
Restoring a key backup merges recovered key records into the local keystore as archived, never active — this prevents a restore from silently starting to sign with an unexpected key. There is no “reveal private key” feature anywhere in the system; the encrypted backup file is the sole export path for key material.
Data Model
Data\LicenceLog.txt — tab-delimited, UTF-8, sole source of truth. Columns: ID, Product, ProductVersion, Domain, LicenceType, ExpiryDate, IssueDate, LicenceKey, KeyVersion, LicenceIdentifier, CustomerName, CompanyName, CustomerEmail, OrderReference, Notes, PdfPath, CreatedBy, CreatedTimestamp, LastModifiedTimestamp, LicenceStatus.
Data\LicenceHistory.txt — append-only, never rewritten. Columns: Timestamp, Action, Product, KeyVersion, Identifier, Domain. Audited actions: MINT, PDF_CREATED, PDF_REGENERATE, DELETE_RECORD, KEY_GENERATED, KEY_ACTIVATED, KEY_ARCHIVED, KEY_RESTORED, TRIAL_OVERRIDE, BACKUP_CREATED, REBUILD_EXECUTED.
Licence identifiers follow LIC-YYYY-NNNNNN, sequential, never reused even after a log record is deleted — deleting a Licence Log row removes the record only and does not revoke the underlying signed licence.
Plugin-Side Verification (Trust Armory example)
Licence format: PREFIX.base64url(payload).base64url(signature) — TAL1. for Trust Armory, SFL1. for Support Funnel. Verification uses sodium_crypto_sign_verify_detached against a list of embedded public keys, tried in order, so a rotated-out key’s licences remain verifiable as long as its public key stays in the array.
Payload fields: v, tier, domain, exp, iat, id. Domain binding is normalised identically on both the minting and verification sides (lowercase, strip protocol/www/path). A domain-change grace period and a trial grace period both extend validity briefly past a literal state change or expiry.
Licensing gates customisation only — never core functionality. The Free tier is a fully working product, not a crippled demo.
Known Limitations
- Client-side bypass: a technically capable customer with file access to their own server can patch the licence check directly. Routine plugin updates overwrite naive patches, but this is friction, not a guarantee — it does not stop someone willing to re-patch after each update or simply not update.
- No concurrency support: Minter assumes one administrator, one active session. Two open sessions writing to the same LicenceLog.txt/LicenceHistory.txt simultaneously is unsupported and can lose data.
- OneDrive sync is supported for redundancy, not for concurrent multi-device editing.
- Recovery depends entirely on the KeyBackups .enc file and its password being current and retrievable — Create Backup (workbook/log/history) does not protect private key material at all.
v1.1 Additions: New Modules
13 new modules and 2 new UserForms were added on top of the originally delivered architecture. Extracted and verified directly from the compiled VBA project (vbaProject.bin) in the uploaded workbook.
| Module | Purpose |
|---|---|
| modHistoryViewer + frmHistoryLog | Full searchable audit-trail UI reading LicenceHistory.txt, newest-first, double-click to jump to a licence or its PDF |
| modViewLicenceDetails + frmLicenceDetails | Formatted licence detail popup with editable Notes field |
| modOpenPdf, modCopyKey, modRegeneratePdf, modDeleteLicence | Per-row actions on the selected Licence Log record |
| modSearch | Live search across all 20 LicenceLog columns; jumps to and selects first match |
| modNavigation | Sheet navigation, including auto-clearing any active filter when entering the Licence Log |
| modEmail | Built-out Outlook draft creation: template placeholder replacement, per-product From-address matching against configured Outlook accounts |
| modButtons, modPdfFolder, modTrialCOunt | Supporting helpers — see caveats below |
Known Issue: Hardcoded Path in Two Modules
modButtons.OpenLatestPdfForProduct and modOpenPdf.OpenSelectedPdf construct the PDF path as:
fullPath = “C:\Minter\” & pdfPath
instead of using modConfig.RootPath(), which every other module (including the original modPDF and the new modPdfFolder) uses to resolve paths relative to wherever the workbook actually lives. This reintroduces a hardcoded dependency on the workbook living at exactly C:\Minter\ for these two specific buttons only — they will fail to locate PDFs if the folder is moved (different drive, renamed folder, OneDrive-synced path) even though the rest of the system continues to resolve paths correctly.
Fix: replace the hardcoded string with modConfig.RootPath() in both locations, matching the pattern already used elsewhere.
Known Issue: Parallel Trial-Count Implementation
The original trial-duplicate check, modDataStore.FindPreviousTrial, reads LicenceLog.txt directly — the authoritative source of truth. The new modTrialCOunt.GetTrialCount instead scans the LicenceLog worksheet directly via a manual row loop.
These should normally agree, since the sheet is a refreshed view of the same file — but they are two independent code paths performing similar logic against two different data sources (file vs. worksheet). If the Excel view is ever stale (the specific scenario the core “Excel refresh fails” narrow-failure handling exists for), a caller using GetTrialCount would silently get a different answer than one using FindPreviousTrial, until Rebuild Workbook From Audit File is run.
No call site for GetTrialCount was found in the extracted modules — it may not yet be wired into the mint flow. Worth confirming whether it’s in active use before two divergent trial-check paths cause a real discrepancy.
Unchanged Core (Verified)
Full line-by-line comparison confirms the following remain exactly as originally designed: the mint sequence and its failure-handling rules, the encrypted key backup/restore flow (AES-256-GCM + PBKDF2), the full audit event set, domain normalisation, key rotation/activation logic, and the DPAPI-bound local keystore. The v1.1 additions are UI/workflow conveniences layered on top — none of them alter the underlying trust model.