{"record":{"id":"29650f769bb0a743","repo":"BeyondDimension/SteamTools","slug":"hotp-authenticator-should-have-a-counter","errorCode":null,"errorMessage":"HOTP authenticator should have a counter","messagePattern":"HOTP authenticator should have a counter","errorType":"exception","errorClass":"ApplicationException","httpStatus":null,"severity":"warning","filePath":"src/BD.WTTS.Client.Plugins.Authenticator/UI/ViewModels/AuthenticatorImportPageViewModel.cs","lineNumber":300,"sourceCode":"                {\n                    issuer = label.Substring(0, p);\n                    label = label[(p + 1)..];\n                }\n\n                // + aren't decoded\n                label = label.Replace(\"+\", \" \");\n\n                var query = HttpUtility.ParseQueryString(uri.Query);\n                string? secret = query[\"secret\"];\n                if (string.IsNullOrEmpty(secret))\n                {\n                    throw new ApplicationException(\"Authenticator does not contain secret\");\n                }\n\n                string? counter = query[\"counter\"];\n                if (uri.Host == \"hotp\" && string.IsNullOrEmpty(counter))\n                {\n                    throw new ApplicationException(\"HOTP authenticator should have a counter\");\n                }\n\n                AuthenticatorDTO authenticatorDto = new();\n\n                AuthenticatorValueDTO auth;\n                if (string.Compare(issuer, \"BattleNet\", StringComparison.OrdinalIgnoreCase) == 0)\n                {\n                    string? serial = query[\"serial\"];\n                    if (string.IsNullOrEmpty(serial))\n                    {\n                        throw new ApplicationException(\"Battle.net Authenticator does not have a serial\");\n                    }\n\n                    serial = serial.ToUpper();\n                    if (Regex.IsMatch(serial, @\"^[A-Z]{2}-?[\\d]{4}-?[\\d]{4}-?[\\d]{4}$\") == false)\n                    {\n                        throw new ApplicationException(\"Invalid serial for Battle.net Authenticator\");\n                    }","sourceCodeStart":282,"sourceCodeEnd":318,"githubUrl":"https://github.com/BeyondDimension/SteamTools/blob/c16ffa08e03b192d23ada290c4969e77f9201f3d/src/BD.WTTS.Client.Plugins.Authenticator/UI/ViewModels/AuthenticatorImportPageViewModel.cs#L282-L318","documentation":"Thrown when an otpauth://hotp/ entry lacks a 'counter' query parameter. HOTP is counter-based, so the counter is mandatory to know which code to generate next; a HOTP URI without it is incomplete and cannot be imported.","triggerScenarios":"Importing an otpauth://hotp/ URI whose query string has no 'counter' key or an empty value.","commonSituations":"Export tool omitted the counter; URI truncated; entry was converted from TOTP incorrectly; counter was set to a non-numeric/empty value.","solutions":["Re-export the HOTP entry with its current counter value included.","Manually append &counter=0 (or the known current counter) to the URI before importing.","If the true counter is unknown, default to 0 and let the user resync after import.","Validate counter presence in the UI for HOTP entries before parsing."],"exampleFix":"// before\nif (uri.Host == \"hotp\" && string.IsNullOrEmpty(counter))\n    throw new ApplicationException(\"HOTP authenticator should have a counter\");\n\n// after: default to 0 with a warning when counter is missing\nif (uri.Host == \"hotp\")\n    counter = string.IsNullOrEmpty(counter) ? \"0\" : counter;","handlingStrategy":"validation","validationCode":"if (uri.Host.Equals(\"hotp\", StringComparison.OrdinalIgnoreCase))\n{\n    var counter = query[\"counter\"];\n    if (string.IsNullOrEmpty(counter) || !long.TryParse(counter, out _))\n        errors.Add(($\"Line {n}: HOTP entry missing a numeric 'counter'.\", line));\n}","typeGuard":"bool HotpHasCounter(Uri u) => !(u.Host == \"hotp\") || !string.IsNullOrEmpty(HttpUtility.ParseQueryString(u.Query)[\"counter\"]);","tryCatchPattern":"try { ParseAuthenticator(line); }\ncatch (ApplicationException ex) when (ex.Message == \"HOTP authenticator should have a counter\")\n{\n    // Optionally default counter to 0 with a warning and retry parse.\n    line = line + (line.Contains(\"?\") ? \"&\" : \"?\") + \"counter=0\";\n    ParseAuthenticator(line);\n}","preventionTips":["For HOTP entries, require and validate a numeric counter before import.","Default missing counters to 0 and let the user resync after import.","Export tools should always include the current counter for HOTP.","Surface a HOTP-specific hint in the UI ('this entry needs a counter')."],"tags":["authenticator","import","validation","hotp","counter"],"backgroundTag":null,"analyzedSha":"c16ffa08e03b192d23ada290c4969e77f9201f3d","analyzedAt":"2026-08-13T11:52:20.410Z","schemaVersion":2},"datasetVersion":"2026-08-13T14:17:21.547Z"}