abpframework/abp · error · AbpException

The BLOB encryption passphrase contains invalid characters (

Error message

The BLOB encryption passphrase contains invalid characters (unpaired surrogates)!

What it means

The encryption passphrase is encoded with strict UTF-8 so every target framework derives the identical PBKDF2 key. A passphrase containing an unpaired UTF-16 surrogate (e.g. a lone \uD800) cannot be encoded to valid UTF-8, so EncoderFallbackException is caught and rethrown as this AbpException. Rejecting it here avoids the framework-dependent behavior where net8+ throws but netstandard2.1 silently substitutes replacement characters, which would produce divergent keys.

Source

Thrown at framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobEncryptionCodec.cs:341

    }

    internal static byte[] DeriveKeyBytes(string passPhrase, byte[] salt, int iterations)
    {
#if NETSTANDARD2_0
        throw new PlatformNotSupportedException("BLOB encryption requires AES-GCM, which is not available on .NET Standard 2.0!");
#else
        // Encode the passphrase to bytes with strict UTF-8 explicitly, so every target
        // framework derives the same key and an invalid passphrase (unpaired surrogates)
        // is rejected the same way — the string overloads differ across frameworks (net8+
        // throws on invalid UTF-16, netstandard2.1 silently replaces it)
        byte[] passwordBytes;
        try
        {
            passwordBytes = StrictUtf8.GetBytes(passPhrase);
        }
        catch (EncoderFallbackException ex)
        {
            throw new AbpException("The BLOB encryption passphrase contains invalid characters (unpaired surrogates)!", ex);
        }

        try
        {
#if NET8_0_OR_GREATER
            return Rfc2898DeriveBytes.Pbkdf2(passwordBytes, salt, iterations, HashAlgorithmName.SHA256, 32);
#else
            using var password = new Rfc2898DeriveBytes(passwordBytes, salt, iterations, HashAlgorithmName.SHA256);
            return password.GetBytes(32);
#endif
        }
        finally
        {
            CryptographicOperations.ZeroMemory(passwordBytes);
        }
#endif
    }

View on GitHub (pinned to 7ed43b1931)

Solutions

  1. Replace the passphrase with plain ASCII or a base64/hex string so every byte round-trips cleanly.
  2. Validate the passphrase before configuring it: ensure no char is a standalone surrogate (char.IsSurrogatePair over the string, or reject any char with IsHighSurrogate/IsLowSurrogate that is unpaired).
  3. If the passphrase must come from an external source, normalize it (e.g. strip/replace invalid sequences) at the boundary where it enters the app.
  4. Source the passphrase from a secrets manager that stores it as UTF-8 bytes rather than letting mid-pipeline string slicing corrupt it.

Example fix

// before
Configure<AbpBlobStoringEncryptionOptions>(o =>
    o.DefaultPassPhrase = badStringFromConfig); // contains lone surrogate

// after
var pass = badStringFromConfig;
if (pass.Any(c => char.IsSurrogate(c) &&
    !((c >= '\uD800' && c <= '\uDBFF') && /* paired check */ false)))
{
    throw new InvalidOperationException("Refusing invalid passphrase");
}
Configure<AbpBlobStoringEncryptionOptions>(o =>
    o.DefaultPassPhrase = Regex.Replace(pass, "[\uD800-\uDFFF]", "")); // or use clean ASCII
Defensive patterns

Strategy: validation

Validate before calling

// Validate a passphrase before configuring it for BLOB encryption.
static bool IsValidPassphrase(string s)
{
    if (string.IsNullOrWhiteSpace(s)) return false;
    for (var i = 0; i < s.Length; i++)
    {
        var c = s[i];
        if (char.IsHighSurrogate(c))
        {
            if (i + 1 >= s.Length || !char.IsLowSurrogate(s[i + 1])) return false; // unpaired high
            i++;
        }
        else if (char.IsLowSurrogate(c))
        {
            return false; // low surrogate without preceding high
        }
    }
    return true;
}

// usage
if (!IsValidPassphrase(pass))
    throw new InvalidOperationException("Passphrase has unpaired surrogates");

Type guard

// Restrict passphrase configuration to validated strings at the boundary.
public sealed record ValidPassphrase
{
    public string Value { get; }
    public ValidPassphrase(string value)
    {
        if (!IsValidPassphrase(value))
            throw new ArgumentException("Passphrase contains unpaired surrogates", nameof(value));
        Value = value;
    }
}
// then: o.DefaultPassPhrase = new ValidPassphrase(raw).Value;

Try / catch

try
{
    Configure<AbpBlobStoringEncryptionOptions>(o => o.DefaultPassPhrase = pass);
    await blob.SaveAsync(name, data);
}
catch (AbpException ex) when (ex.Message.Contains("invalid characters (unpaired surrogates)"))
{
    // configuration-time failure; fix the passphrase source, do not retry with the same value
    logger.LogError(ex, "Refusing to start: encryption passphrase is malformed");
    throw;
}

Prevention

When it happens

Trigger: Passing a passphrase containing a lone surrogate char to UseEncryption(...), to AbpBlobStoringEncryptionOptions.DefaultPassPhrase, or returning one from a custom IBlobEncryptionKeyProvider. Any key source whose value flows into BlobEncryptionCodec.DeriveKeyBytes triggers it.

Common situations: Programmatic passphrase built from binary/copy-paste that got sliced mid-surrogate pair; mojibake from mis-decoded strings; passphrase sourced from a corrupted config value or external secret store.

Understand the failure class

Related errors


AI-assisted analysis of abpframework/abp@7ed43b1931 (2026-08-13). Data as JSON: /api/errors/e3eeaa0ed9f7237d. Report an issue: GitHub.