Skip to content

Windows rejects a 2400-char base64 token as over the 2560-byte limit: JSON escaping writes every '+' and non-ASCII char as \uXXXX (6 bytes) #175

Description

@matt-edmondson

What's wrong

CredentialSerialization.BuildOptions (CredentialCache/Storage/CredentialSerialization.cs:~239-249) creates JsonSerializerOptions without setting Encoder, so System.Text.Json uses JavaScriptEncoder.Default. That encoder escapes these characters as \uXXXX, which is 6 bytes each:

  • +, <, >, & and '
  • every non-ASCII character

WindowsCredentialStore.Save (Storage/WindowsCredentialStore.cs:75-81) checks the escaped blob against CRED_MAX_CREDENTIAL_BLOB_SIZE (2560). The README's platform notes say the cap applies to "tokens larger than that". In practice the usable size is much smaller, and it depends on what the secret contains.

Failure scenario

Measured on net10.0:

  • A standard base64 token (alphabet includes +) of 2400 characters serializes to 2662 bytes. Save throws CredentialStoreException on Windows, while Linux and macOS accept the same credential. With UnsafeRelaxedJsonEscaping it is 2442 bytes and fits.
  • A 2000-char base64 token containing 36 + becomes 2222 bytes, and the JSON contains + where the + was.
  • {"Username":"пользователь","Password":"密码密码密码"} becomes 178 bytes, with every character written as \u. Cyrillic grows from 2 to 6 bytes per character and CJK from 3 to 6, so non-ASCII secrets reach the limit 2–3× sooner.

Suggested fix

  • Set Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping in BuildOptions. The payload is never embedded in HTML or script, so HTML-safe escaping buys nothing.
  • This is backward compatible: the deserializer reads both escaped and unescaped forms, so credentials already stored still load.
  • Optionally reword the README note to say the limit counts the serialized (UTF-8 JSON) size.

Acceptance criteria

  • A test asserts that serializing a credential whose password contains + and non-ASCII characters produces raw UTF-8 rather than \u escapes.
  • A test asserts that a previously serialized, escaped blob still deserializes.

Related, not a duplicate: #165 covers scrubbing on the over-limit path.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions