【问题标题】:DPAPI fails with CryptographicException when trying to decrypt Chrome cookies尝试解密 Chrome cookie 时,DPAPI 失败并出现 CryptographicException
【发布时间】:2020-02-14 16:48:09
【问题描述】:

我正在尝试从我的 Chrome 浏览器获取会话。我可以在开发者工具中看到 2 个 cookie 文件。但这对于用户从浏览器获取 cookie 值是不方便的,我想用代码来做。所以我使用此代码获取 Chrome 默认配置文件 cookie sqlite DB:

string local = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
string path = @"Google\Chrome\User Data\Default\Cookies";

path = Path.Combine(local, path);

接下来我创建 SQLite 连接并请求

var cmd = new SQLiteCommand("SELECT encrypted_value, name FROM cookies WHERE host_key = 'my_host_ip'", con);

然后我阅读结果

byte[] encryptedCookie = (byte[])r.GetValue(r.GetOrdinal("encrypted_value"));

并尝试解密它:

var decodedData = ProtectedData.Unprotect(encryptedCookie, null, DataProtectionScope.CurrentUser);
var plainText = Encoding.ASCII.GetString(decodedData);

这里我遇到了异常

System.Security.Cryptography.CryptographicException

我知道我必须在启动浏览器的同一用户帐户下(在同一台机器上)解密 cookie 内容,并为此使用参数 DataProtectionScope.CurrentUser

我在调试器中看到了 63 个字节(在 encryptedCookie 数组中),我还在 SQLite DB BLOB 字段中看到了这个字节。 但是Unprotect 方法会抛出System.Security.Cryptography.CryptographicException: Invalid data 错误。

我的代码在我办公室的 5 台不同的 PC 上运行良好(win10、win7),但在我的开发人员 PC 上无法运行(win10、vs2019)。

我认为问题出在我的 Windows 设置或其他地方,而不是我的代码中。那我做错了什么?

有趣的注释——我发现了做同样事情的 PowerShell 脚本(通过Add-Type -AssemblyName System.Security)——获取 cookie 并解密它。此脚本在 5 台办公室 PC 上也可以正常工作,但在我的 PC 上无法正常工作。

我的 Windows 安装是新的,我没有 AV 软件。我们连接到同一个企业域并且我们具有相同的安全设置。

UPD 1 一个小实验:

  1. 从 Chrome 浏览器获取 cookie 值(32 个字符,JSESSIONID)
  2. 创建一个简单的应用程序,使用CurrentUser 保护范围保护此值。现在我有一个 178 字节的数组(结果 #1)
  3. 使用 a) https://sqliteonline.com/ 和 b) DataBase.Net 桌面应用程序查看 Chrome 的 cookie 数据库。这两种方法给了我相同的结果:只有 63 字节的加密 cookie 数据(结果 #2)。我也可以使用 System.Data.SQLite 在我的 c# 应用程序中获得相同的结果

所以,结果的长度或内容不相等 结果 #1 != 结果 #2

看起来 Chrome 的 cookie 值受不同范围保护(可能是管理员帐户?),但我在 Chrome 进程的任务管理器中看到我的用户帐户名称

附:我使用.net 4.7.2

UPD 2 我在 Chromium 资源中找到了这种方法

bool OSCrypt::DecryptString(const std::string& ciphertext,
                            std::string* plaintext) {
  if (!base::StartsWith(ciphertext, kEncryptionVersionPrefix,
                        base::CompareCase::SENSITIVE))
    return DecryptStringWithDPAPI(ciphertext, plaintext);

  crypto::Aead aead(crypto::Aead::AES_256_GCM);

  auto key = GetEncryptionKeyInternal();
  aead.Init(&key);

  // Obtain the nonce.
  std::string nonce =
      ciphertext.substr(sizeof(kEncryptionVersionPrefix) - 1, kNonceLength);
  // Strip off the versioning prefix before decrypting.
  std::string raw_ciphertext =
      ciphertext.substr(kNonceLength + (sizeof(kEncryptionVersionPrefix) - 1));

  return aead.Open(raw_ciphertext, nonce, std::string(), plaintext);
}

所以 DPAPI 仅在 BLOB NOT 以 v10 字符开头时使用。但是我的 cookie BLOB 以 v10 字符开头,并且根据代码,使用了另一种加密算法,但我不明白为什么。

【问题讨论】:

  • 我首先要让 MySQL 脱离循环——你能加密然后解密你计算机上的任意字节数组吗?只需调用 Protect 然后 Unprotect 结果。
  • @JonSkeet 是的,我创建了一个简单的示例,我在其中保护数据数组,然后在一个控制台应用程序中使用相同的熵(null)和相同的密钥取消保护它。一切正常
  • 所以你需要弄清楚事情链的哪一步失败了。首先确保您可以完全可靠地重现它 - 然后您可以在输入和输出的过程中记录字节(例如使用 base64 获取 ASCII),并查看它们的变化。
  • 没有 .NET 7.2。也许您的意思是您使用的是确实存在的 C# 7.2。 .NET 和 C# 版本目前完全不同。
  • 我真的不知道 - 我不能完全按照您正在经历的步骤,或者数据的来源。希望其他人能够为您提供更多帮助。

标签: c# dpapi


【解决方案1】:

我终于明白了。根据 Chromium 来源,有两种方法用于解密 cookie 值。

  1. 如果cookie值以v10字符开头,我们使用AES_256_GCM
  2. 否则,使用DPAPI

对于第一种方法,我们需要 key 和 nonce。密钥位于 Google Chrome 文件中,nonce 位于加密的 cookie 值中。

我还不清楚 - 是什么决定了使用哪种方法

【讨论】:

    【解决方案2】:

    对于正在寻找代码的人,我正在扩展 Cerberus 答案。从 Chrome 80 版本开始,cookie 使用 AES256-GCM 算法加密,AES 加密密钥使用 DPAPI 加密系统加密,加密后的密钥存储在“Local State”文件中。

    byte[] encryptedData=<data stored in cookie file>
    string encKey = File.ReadAllText(localAppDataPath + @"\Google\Chrome\User Data\Local State");
    encKey = JObject.Parse(encKey)["os_crypt"]["encrypted_key"].ToString();
    var decodedKey = System.Security.Cryptography.ProtectedData.Unprotect(Convert.FromBase64String(encKey).Skip(5).ToArray(), null, System.Security.Cryptography.DataProtectionScope.LocalMachine);
    _cookie = _decryptWithKey(encryptedData, decodedKey, 3);
    

    密钥大小为 256 位。加密消息格式为,payload('v12')+nonce(12 bytes)+cipherText

    private string _decryptWithKey(byte[] message, byte[] key, int nonSecretPayloadLength)
    {
        const int KEY_BIT_SIZE = 256;
        const int MAC_BIT_SIZE = 128;
        const int NONCE_BIT_SIZE = 96;
    
        if (key == null || key.Length != KEY_BIT_SIZE / 8)
            throw new ArgumentException(String.Format("Key needs to be {0} bit!", KEY_BIT_SIZE), "key");
        if (message == null || message.Length == 0)
            throw new ArgumentException("Message required!", "message");
    
        using (var cipherStream = new MemoryStream(message))
        using (var cipherReader = new BinaryReader(cipherStream))
        {
            var nonSecretPayload = cipherReader.ReadBytes(nonSecretPayloadLength);
            var nonce = cipherReader.ReadBytes(NONCE_BIT_SIZE / 8);
            var cipher = new GcmBlockCipher(new AesEngine());
            var parameters = new AeadParameters(new KeyParameter(key), MAC_BIT_SIZE, nonce);
            cipher.Init(false, parameters);
            var cipherText = cipherReader.ReadBytes(message.Length);
            var plainText = new byte[cipher.GetOutputSize(cipherText.Length)];
            try
            {
                var len = cipher.ProcessBytes(cipherText, 0, cipherText.Length, plainText, 0);
                cipher.DoFinal(plainText, len);
            }
            catch (InvalidCipherTextException)
            {
                return null;
            }
            return Encoding.Default.GetString(plainText);
        }
    }
    

    需要的包

    1) Newtonsoft JSON .net

    2) Bouncy Castle Crypto package

    【讨论】:

    • 谢谢!我使用相同的加密 API。对于那些想要使用此代码的人 1) 您需要 Bouncy Castle Crypto API (github.com/neoeinstein/bouncycastle) 2) 您需要 Newtonsoft JSON.net (newtonsoft.com/json)。请修复密钥大小为 256 位,而不是字节
    猜你喜欢
    • 2012-10-05
    • 2021-06-13
    • 1970-01-01
    • 2021-06-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-23
    • 2015-08-04
    相关资源
    最近更新 更多