mgth/LittleBigMouse · error · CryptographicException

Protected payload has no scheme.

Error message

Protected payload has no scheme.

What it means

Unprotect strips the envelope Prefix and then looks for a '.' separating the scheme name (e.g. DPAPI or AES-GCM) from the base64 payload. If the body contains no dot, the envelope is malformed and this CryptographicException is thrown. The scheme selector is mandatory, so an envelope without it cannot be decrypted.

Solutions

  1. Re-encrypt the secret with the current SecretProtector.Protect so a valid scheme-prefixed envelope is produced.
  2. Inspect the envelope string and restore the missing '<scheme>.' segment between the prefix and the base64 payload.
  3. Delete the stored value and re-enter the secret so the app saves it in the current format.
  4. Catch CryptographicException on load and treat the value as unreadable, starting fresh per the API contract.

Example fix

// before (no scheme)
enc:QmFzZTY0UGF5bG9hZA==
// after (scheme separated by dot)
enc:aesgcm.QmFzZTY0UGF5bG9hZA==
Defensive patterns

Strategy: validation

Validate before calling

// envelope must be prefix + scheme + '.' + base64
var body = envelope.Substring(SecretProtector.Prefix.Length);
if (!body.Contains('.'))
    throw new FormatException("Envelope is missing its scheme segment; re-protect the value.");

Type guard

static bool HasSchemeSegment(string envelope, string prefix)
{
    if (!envelope.StartsWith(prefix, StringComparison.Ordinal)) return false;
    var idx = envelope.IndexOf('.', prefix.Length);
    return idx > prefix.Length && idx < envelope.Length - 1;
}

Try / catch

try
{
    secret = protector.Unprotect(envelope);
}
catch (CryptographicException ex) when (ex.Message.Contains("no scheme"))
{
    Log.LogWarning("Malformed envelope (missing scheme); re-protecting required.");
    secret = null;
}

Prevention

When it happens

Trigger: Passing a string that has the envelope prefix but only a bare base64 body with no scheme tag — e.g. an envelope built by hand, produced by an incompatible app version, or corrupted so the dot was lost.

Common situations: Settings files edited or migrated between app versions with different envelope layouts, secrets copied from another tool's format, or partial string truncation that removed the scheme separator.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of mgth/LittleBigMouse@7a42f01d47 (2026-09-16). Data as JSON: /api/errors/4e7926d8e8ad3cf7. Report an issue: GitHub.

Appendix: source

Thrown at LittleBigMouse.Plugins/LittleBigMouse.Plugins.Core/SecretProtector.cs:92

            payload.AsSpan(NonceLength + TagLength),
            payload.AsSpan(NonceLength, TagLength));

        return Prefix + AesGcmScheme + "." + Convert.ToBase64String(payload);
    }

    /// <summary>
    /// Reverses <see cref="Protect"/>. Throws — on a foreign scheme, a wrong key, a truncated
    /// or tampered payload — rather than returning something plausible; callers treat that the
    /// same way they already treat unreadable settings, by starting fresh.
    /// </summary>
    public string Unprotect(string envelope)
    {
        if (!IsProtected(envelope))
            throw new CryptographicException("Not a protected payload.");

        var body = envelope.AsSpan(Prefix.Length);
        var separator = body.IndexOf('.');
        if (separator < 0) throw new CryptographicException("Protected payload has no scheme.");

        var scheme = body[..separator].ToString();
        var payload = Convert.FromBase64String(body[(separator + 1)..].ToString());

        switch (scheme)
        {
            case DpapiScheme when OperatingSystem.IsWindows():
                return Encoding.UTF8.GetString(
                    ProtectedData.Unprotect(payload, null, DataProtectionScope.CurrentUser));

            case AesGcmScheme:
                if (payload.Length < NonceLength + TagLength)
                    throw new CryptographicException("Protected payload is truncated.");

                var plain = new byte[payload.Length - NonceLength - TagLength];
                using (var aes = new AesGcm(ReadKey() ?? throw new CryptographicException(
                           $"No key at {_keyFilePath} to read this payload with."), TagLength))
                {

View on GitHub (pinned to 7a42f01d47)