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
- Replace the passphrase with plain ASCII or a base64/hex string so every byte round-trips cleanly.
- 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).
- 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.
- 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
- Source passphrases from a secrets manager that stores them as UTF-8 bytes, not from ad-hoc string slicing.
- Prefer ASCII or base64/hex passphrases so no surrogate can ever appear.
- Add a startup assertion that the resolved passphrase is valid before the app accepts traffic.
- Never build passphrases by slicing at arbitrary char indices (can split a surrogate pair).
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- BLOB encryption is enabled, but no passphrase could be resol
- The encrypted BLOB is corrupted or has an invalid format: in
- Unknown BLOB encryption key source: {source}!
- code length overflow. (${buffer.getLengthInBits()}>${totalDa
- Expected Dapr App API Token is not provided! Dapr should set
AI-assisted analysis of abpframework/abp@7ed43b1931 (2026-08-13).
Data as JSON: /api/errors/e3eeaa0ed9f7237d.
Report an issue: GitHub.