如何在 PHP 中解密 C# StringCipher 库加密的字符串

来源:SEO作者:又改需求头衔:程序员
导读:本期聚焦于又改需求创作的《如何在 PHP 中解密 C# StringCipher 库加密的字符串》,敬请观看详情。如果业务中需要把 C# 服务端使用 StringCipher 工具类加密后的数据交给 PHP 解密,直接用 openssl_decrypt 往往会因为密钥派生和密文结构不一致而报错。StringCipher 的常见实现并不是简单地把密码当作密钥,而是通过 PBKDF2 从口令加盐派生 AES 密钥,有些版本还会把随机盐和 IV 一起拼进密文里。要让 PHP 正确解密,必须先弄清 C# 端的分组模式、填充方式、迭代次数以及盐和 IV 的存放顺序。这篇文章以一个典型 StringCipher 实现为例,完整展示从拆分 Base64 数据、派生密钥到调用 openssl_decrypt 的过程,并说明两个平台在哈希算法、编码和 Padding 上的对齐方法,帮助排查 bad decrypt 等常见问题。

在跨语言协作中,PHP 开发人员有时需要解密由 C# 服务端生成的密文。StringCipher 是 C# 社区常见的字符串加密封装,但它并不是一个官方标准库,不同实现可能采用不同的密钥派生和密文组织方式。如果只是简单地把口令当作 AES 密钥去调用 openssl_decrypt,通常会得到乱码或直接报错。需要先梳理 C# 端的加密细节,再在 PHP 端逐一对应。

如何在 PHP 中解密 C# StringCipher 库加密的字符串

先确认 C# 端加密流程

StringCipher 的典型实现大多采用 AES 对称加密,并配合 PBKDF2 派生密钥。完整的加密过程通常包含这几步:生成一个随机盐,生成一个随机 IV,使用口令和盐通过 PBKDF2 派生 32 字节密钥,然后执行 AES-CBC 加密,最后把盐、IV 和密文按顺序拼接并进行 Base64 编码。解密的顺序则是逆向操作,必须先还原出盐、IV 和真正的密文。

下面是一段常见的 C# StringCipher 加密代码,它使用 Rfc2898DeriveBytes 派生密钥,并用 Aes 类进行 CBC 模式加密。注意这里盐和 IV 都放在密文前面,长度各为 16 字节。

public static string Encrypt(string plainText, string passPhrase)
{
    byte[] salt = new byte[16];
    using (var rng = RandomNumberGenerator.Create())
    {
        rng.GetBytes(salt);
    }

    byte[] iv = new byte[16];
    using (var rng = RandomNumberGenerator.Create())
    {
        rng.GetBytes(iv);
    }

    byte[] key;
    using (var deriveBytes = new Rfc2898DeriveBytes(passPhrase, salt, 10000))
    {
        key = deriveBytes.GetBytes(32);
    }

    using (Aes aes = Aes.Create())
    {
        aes.Key = key;
        aes.IV = iv;
        aes.Mode = CipherMode.CBC;
        aes.Padding = PaddingMode.PKCS7;

        using (var encryptor = aes.CreateEncryptor())
        using (var ms = new MemoryStream())
        {
            using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
            using (var sw = new StreamWriter(cs, Encoding.UTF8))
            {
                sw.Write(plainText);
            }

            byte[] cipherBytes = ms.ToArray();
            byte[] result = new byte[salt.Length + iv.Length + cipherBytes.Length];
            Buffer.BlockCopy(salt, 0, result, 0, salt.Length);
            Buffer.BlockCopy(iv, 0, result, salt.Length, iv.Length);
            Buffer.BlockCopy(cipherBytes, 0, result, salt.Length + iv.Length, cipherBytes.Length);
            return Convert.ToBase64String(result);
        }
    }
}

从这段代码可以提取出几个关键信息:迭代次数是 10000,PBKDF2 使用的哈希算法默认是 SHA1,AES 密钥长度为 256 位,分组模式为 CBC,填充方式为 PKCS7。这些参数必须与 PHP 端完全一致,否则无法得到正确的明文。

还要特别注意,不同版本的 StringCipher 可能在盐和 IV 的存放顺序、长度,或者是否将盐固定写在代码中等方面存在差异。如果解密失败,第一步应该是阅读 C# 源码,而不是盲目调整 PHP 参数。

在 PHP 中实现对应解密

解密流程可以分为四个步骤:先对 Base64 字符串进行严格解码,然后从字节数组中切出前 16 字节盐、接下来 16 字节 IV 和剩余密文,接着用 hash_pbkdf2 函数从口令和盐派生 32 字节密钥,最后调用 openssl_decrypt 还原明文。

下面给出完整的 PHP 解密函数。为了方便与 C# 端对齐,这里同样使用 SHA1 作为 PBKDF2 的哈希算法,迭代次数为 10000,密钥长度 32 字节。

function decryptStringCipher($base64Data, $passPhrase)
{
    $raw = base64_decode($base64Data, true);
    if ($raw === false) {
        throw new Exception('Base64 解码失败');
    }

    if (strlen($raw) < 32) {
        throw new Exception('密文长度不足');
    }

    $salt = substr($raw, 0, 16);
    $iv = substr($raw, 16, 16);
    $cipherText = substr($raw, 32);

    $key = hash_pbkdf2('sha1', $passPhrase, $salt, 10000, 32, true);

    $plainText = openssl_decrypt(
        $cipherText,
        'aes-256-cbc',
        $key,
        OPENSSL_RAW_DATA,
        $iv
    );

    if ($plainText === false) {
        throw new Exception('解密失败,请检查密钥、IV 或密文结构');
    }

    return $plainText;
}

代码中 base64_decode 的第二个参数设为 true,可以避免因非法字符导致函数默默返回空值。切片的长度必须与 C# 端保持一致,这里都是 16 字节。如果 C# 端盐或 IV 的长度不同,比如使用了 32 字节 IV,PHP 切片也要相应调整。

hash_pbkdf2 的最后一个参数 true 表示返回原始二进制字节,而不是十六进制字符串。这一点非常重要,因为 openssl_decrypt 需要二进制密钥。传 OPENSSL_RAW_DATA 表示输入和输出都是原始字节,不会额外做 Base64 处理,这与 C# 端拼接的密文格式相匹配。

对齐哈希、编码和填充方式

PBKDF2 中的哈希算法直接影响派生密钥。C# 的 Rfc2898DeriveBytes 默认使用 SHA1,而 PHP 的 hash_pbkdf2 默认也是 SHA1,所以这个参数容易对齐。但如果 C# 端在构造函数中指定了 HashAlgorithmName.SHA256,PHP 就必须把第一个参数改成 sha256。迭代次数不一致也会产生完全不同的密钥,差一位都解不开。

字符串编码是另一个容易忽略的问题。C# 代码中使用 Encoding.UTF8 把明文转换为字节,PHP 端在解密得到明文后,字符串本身就是 UTF-8 字节序列。只要两端都与 UTF-8 保持一致,通常不会出现乱码。如果 C# 端使用了 Encoding.Unicode 或其他代码页,PHP 需要在解密后手动转换,比如使用 mb_convert_encoding。

填充方式方面,C# 的 PaddingMode.PKCS7 与 OpenSSL 默认的 PKCS7 填充完全一致。调用 openssl_decrypt 时不要加 OPENSSL_ZERO_PADDING,否则 OpenSSL 不会自动去除填充,反而可能需要在明文末尾手动处理补位。保持默认选项即可。

常见错误与排查方法

最典型的错误就是解密返回 false 或抛出 bad decrypt。这通常意味着密钥、IV 或密文切片有误。排查时可以在 PHP 端把 bin2hex 输出的盐、IV 和密钥与 C# 端通过调试代码输出的十六进制值进行比对。任何一段不一致,都能快速定位到派生参数还是切片位置的问题。

Base64 解码失败也时有发生,尤其是在密文被存储到数据库或经过网络传输时。C# 的 Convert.ToBase64String 输出不带换行,但如果中间经过一些模板系统,可能会插入空格或换行符。可以在 base64_decode 之前先使用 preg_replace 移除所有空白字符,再传入解码函数。

如果解密结果看起来是乱码但解密没有报错,往往是编码不匹配。可以先确认 PHP 端输出的字节是否与 C# 端预期的 UTF-8 一致,必要时输出 bin2hex($plainText) 与 C# 端加密前的字节对比。只要密钥派生、切片和算法都对齐,跨语言解密并不复杂。

PHP解密C# AESStringCipher跨语言解密PBKDF2密钥派生修改时间:2026-09-17 20:48:04

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0917/58547.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。