{"record":{"id":"e3eeaa0ed9f7237d","repo":"abpframework/abp","slug":"the-blob-encryption-passphrase-contains-invalid-ch","errorCode":null,"errorMessage":"The BLOB encryption passphrase contains invalid characters (unpaired surrogates)!","messagePattern":"The BLOB encryption passphrase contains invalid characters \\(unpaired surrogates\\)!","errorType":"exception","errorClass":"AbpException","httpStatus":null,"severity":"error","filePath":"framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobEncryptionCodec.cs","lineNumber":341,"sourceCode":"    }\n\n    internal static byte[] DeriveKeyBytes(string passPhrase, byte[] salt, int iterations)\n    {\n#if NETSTANDARD2_0\n        throw new PlatformNotSupportedException(\"BLOB encryption requires AES-GCM, which is not available on .NET Standard 2.0!\");\n#else\n        // Encode the passphrase to bytes with strict UTF-8 explicitly, so every target\n        // framework derives the same key and an invalid passphrase (unpaired surrogates)\n        // is rejected the same way — the string overloads differ across frameworks (net8+\n        // throws on invalid UTF-16, netstandard2.1 silently replaces it)\n        byte[] passwordBytes;\n        try\n        {\n            passwordBytes = StrictUtf8.GetBytes(passPhrase);\n        }\n        catch (EncoderFallbackException ex)\n        {\n            throw new AbpException(\"The BLOB encryption passphrase contains invalid characters (unpaired surrogates)!\", ex);\n        }\n\n        try\n        {\n#if NET8_0_OR_GREATER\n            return Rfc2898DeriveBytes.Pbkdf2(passwordBytes, salt, iterations, HashAlgorithmName.SHA256, 32);\n#else\n            using var password = new Rfc2898DeriveBytes(passwordBytes, salt, iterations, HashAlgorithmName.SHA256);\n            return password.GetBytes(32);\n#endif\n        }\n        finally\n        {\n            CryptographicOperations.ZeroMemory(passwordBytes);\n        }\n#endif\n    }\n","sourceCodeStart":323,"sourceCodeEnd":359,"githubUrl":"https://github.com/abpframework/abp/blob/7ed43b1931b9df46a50c0c59148a18645641d0df/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobEncryptionCodec.cs#L323-L359","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nConfigure<AbpBlobStoringEncryptionOptions>(o =>\n    o.DefaultPassPhrase = badStringFromConfig); // contains lone surrogate\n\n// after\nvar pass = badStringFromConfig;\nif (pass.Any(c => char.IsSurrogate(c) &&\n    !((c >= '\\uD800' && c <= '\\uDBFF') && /* paired check */ false)))\n{\n    throw new InvalidOperationException(\"Refusing invalid passphrase\");\n}\nConfigure<AbpBlobStoringEncryptionOptions>(o =>\n    o.DefaultPassPhrase = Regex.Replace(pass, \"[\\uD800-\\uDFFF]\", \"\")); // or use clean ASCII","handlingStrategy":"validation","validationCode":"// Validate a passphrase before configuring it for BLOB encryption.\nstatic bool IsValidPassphrase(string s)\n{\n    if (string.IsNullOrWhiteSpace(s)) return false;\n    for (var i = 0; i < s.Length; i++)\n    {\n        var c = s[i];\n        if (char.IsHighSurrogate(c))\n        {\n            if (i + 1 >= s.Length || !char.IsLowSurrogate(s[i + 1])) return false; // unpaired high\n            i++;\n        }\n        else if (char.IsLowSurrogate(c))\n        {\n            return false; // low surrogate without preceding high\n        }\n    }\n    return true;\n}\n\n// usage\nif (!IsValidPassphrase(pass))\n    throw new InvalidOperationException(\"Passphrase has unpaired surrogates\");","typeGuard":"// Restrict passphrase configuration to validated strings at the boundary.\npublic sealed record ValidPassphrase\n{\n    public string Value { get; }\n    public ValidPassphrase(string value)\n    {\n        if (!IsValidPassphrase(value))\n            throw new ArgumentException(\"Passphrase contains unpaired surrogates\", nameof(value));\n        Value = value;\n    }\n}\n// then: o.DefaultPassPhrase = new ValidPassphrase(raw).Value;","tryCatchPattern":"try\n{\n    Configure<AbpBlobStoringEncryptionOptions>(o => o.DefaultPassPhrase = pass);\n    await blob.SaveAsync(name, data);\n}\ncatch (AbpException ex) when (ex.Message.Contains(\"invalid characters (unpaired surrogates)\"))\n{\n    // configuration-time failure; fix the passphrase source, do not retry with the same value\n    logger.LogError(ex, \"Refusing to start: encryption passphrase is malformed\");\n    throw;\n}","preventionTips":["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)."],"tags":["crypto","configuration","unicode","passphrase"],"backgroundTag":null,"analyzedSha":"7ed43b1931b9df46a50c0c59148a18645641d0df","analyzedAt":"2026-08-13T16:26:11.351Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}