{"record":{"id":"4a8cd1cd580efb68","repo":"mgth/LittleBigMouse","slug":"not-a-protected-payload","errorCode":null,"errorMessage":"Not a protected payload.","messagePattern":"Not a protected payload\\.","errorType":"exception","errorClass":"CryptographicException","httpStatus":null,"severity":"error","filePath":"LittleBigMouse.Plugins/LittleBigMouse.Plugins.Core/SecretProtector.cs","lineNumber":88,"sourceCode":"        using var aes = new AesGcm(GetOrCreateKey(), TagLength);\n        aes.Encrypt(\n            nonce,\n            bytes,\n            payload.AsSpan(NonceLength + TagLength),\n            payload.AsSpan(NonceLength, TagLength));\n\n        return Prefix + AesGcmScheme + \".\" + Convert.ToBase64String(payload);\n    }\n\n    /// <summary>\n    /// Reverses <see cref=\"Protect\"/>. Throws — on a foreign scheme, a wrong key, a truncated\n    /// or tampered payload — rather than returning something plausible; callers treat that the\n    /// same way they already treat unreadable settings, by starting fresh.\n    /// </summary>\n    public string Unprotect(string envelope)\n    {\n        if (!IsProtected(envelope))\n            throw new CryptographicException(\"Not a protected payload.\");\n\n        var body = envelope.AsSpan(Prefix.Length);\n        var separator = body.IndexOf('.');\n        if (separator < 0) throw new CryptographicException(\"Protected payload has no scheme.\");\n\n        var scheme = body[..separator].ToString();\n        var payload = Convert.FromBase64String(body[(separator + 1)..].ToString());\n\n        switch (scheme)\n        {\n            case DpapiScheme when OperatingSystem.IsWindows():\n                return Encoding.UTF8.GetString(\n                    ProtectedData.Unprotect(payload, null, DataProtectionScope.CurrentUser));\n\n            case AesGcmScheme:\n                if (payload.Length < NonceLength + TagLength)\n                    throw new CryptographicException(\"Protected payload is truncated.\");\n","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/mgth/LittleBigMouse/blob/7a42f01d47d99d223b8ee33ba4019af82adf1c48/LittleBigMouse.Plugins/LittleBigMouse.Plugins.Core/SecretProtector.cs#L70-L106","documentation":"SecretProtector.Unprotect decrypts envelope strings that were produced by Protect and therefore start with a known Prefix. If the input does not carry that prefix, IsProtected returns false and the method throws this CryptographicException instead of attempting to parse plaintext. Callers are expected to treat it as 'not encrypted yet' and start fresh, per the XML docs.","triggerScenarios":"Calling Unprotect on a plain (never-encrypted) string, on a value stored by an older app version before encryption was introduced, or on a value that lost its prefix through truncation/transformation.","commonSituations":"Upgrading from a pre-encryption version whose settings file holds plaintext secrets, manually copying a secret without its envelope prefix, or a config migration stripping the prefix.","solutions":["Call IsProtected(envelope) before Unprotect and skip/decrypt accordingly, treating plaintext as already-unprotected.","Check the stored value actually includes the prefix (e.g. starts with 'enc:' or whatever Prefix is) and re-encrypt it with Protect.","Re-enter the secret in the app so it is saved in the current protected envelope format.","Wrap Unprotect in try-catch for CryptographicException and fall back to treating the value as plaintext."],"exampleFix":"// before\nvar secret = protector.Unprotect(storedValue);\n// after\nvar secret = SecretProtector.IsProtected(storedValue)\n    ? protector.Unprotect(storedValue)\n    : storedValue;","handlingStrategy":"type-guard","validationCode":"// only unprotect envelopes that actually carry the prefix\nif (!SecretProtector.IsProtected(envelope))\n    return envelope; // already plaintext","typeGuard":"static bool IsProtectedEnvelope(string? s) =>\n    !string.IsNullOrEmpty(s) && s.StartsWith(SecretProtector.Prefix, StringComparison.Ordinal);","tryCatchPattern":"try\n{\n    secret = protector.Unprotect(envelope);\n}\ncatch (CryptographicException)\n{\n    secret = envelope; // treat as plaintext and start fresh\n}","preventionTips":["Always round-trip secrets through Protect/Unprotect; never splice envelope strings manually.","Check IsProtected before calling Unprotect when values may predate encryption.","When migrating config versions, re-Protect every value read in the old format."],"tags":["cryptography","dpapi","aes-gcm","settings"],"backgroundTag":"invalid-argument-format","analyzedSha":"7a42f01d47d99d223b8ee33ba4019af82adf1c48","analyzedAt":"2026-09-16T00:35:00.514Z","contentChangedAt":"2026-09-16T00:35:00.514Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}