尝试解密 Chrome cookie 时,DPAPI 失败并出现 CryptographicException
DPAPI fails with CryptographicException when trying to decrypt Chrome cookies
我正在尝试从我的 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
一个小实验:
- 从 Chrome 浏览器获取 cookie 值(32 个字符,JSESSIONID)
- 创建一个使用
CurrentUser
保护范围保护该值的简单应用。现在我有一个 178 字节的数组(结果 #1)
- 使用 a) https://sqliteonline.com/ 和 b) DataBase.Net 桌面应用查看 Chrome 的 cookie 数据库。这两种方法给出了相同的结果:只有 63 字节的加密 cookie 数据(结果 #2)。我也可以使用
System.Data.SQLite
在我的 C# 应用程序中获得相同的结果
所以,结果在长度或内容上都不相等
结果 #1 != 结果 #2
看起来像 Chrome 的 cookie 值受到不同范围的保护(也许是管理员帐户?),但我在 Chrome 的进程 [=27= 的任务管理器中看到我的用户帐户名]
P.S。我使用 .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 不以 v10
个字符开头时使用。但是我的 cookie BLOB 以 v10
个字符开头,并且根据代码,使用了另一种加密算法,但我不明白为什么。
我终于明白了。根据 Chromium 来源,有两种方法用于解密 cookie 值。
- 如果 cookie 值以
v10
个字符开头,我们使用 AES_256_GCM
- 否则,使用
DPAPI
对于第一种方法,我们需要密钥和随机数。密钥位于 Google Chrome 文件中,随机数位于加密的 cookie 值中。
我仍然不清楚 - 是什么决定使用哪种方法
对于正在寻找代码的人,我正在扩展 Cerberus 的答案。 Chrome80版本开始,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 位。加密消息格式为,pay load('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);
}
}
需要的包
我正在尝试从我的 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 一个小实验:
- 从 Chrome 浏览器获取 cookie 值(32 个字符,JSESSIONID)
- 创建一个使用
CurrentUser
保护范围保护该值的简单应用。现在我有一个 178 字节的数组(结果 #1) - 使用 a) https://sqliteonline.com/ 和 b) DataBase.Net 桌面应用查看 Chrome 的 cookie 数据库。这两种方法给出了相同的结果:只有 63 字节的加密 cookie 数据(结果 #2)。我也可以使用
System.Data.SQLite
在我的 C# 应用程序中获得相同的结果
所以,结果在长度或内容上都不相等 结果 #1 != 结果 #2
看起来像 Chrome 的 cookie 值受到不同范围的保护(也许是管理员帐户?),但我在 Chrome 的进程 [=27= 的任务管理器中看到我的用户帐户名]
P.S。我使用 .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 不以 v10
个字符开头时使用。但是我的 cookie BLOB 以 v10
个字符开头,并且根据代码,使用了另一种加密算法,但我不明白为什么。
我终于明白了。根据 Chromium 来源,有两种方法用于解密 cookie 值。
- 如果 cookie 值以
v10
个字符开头,我们使用AES_256_GCM
- 否则,使用
DPAPI
对于第一种方法,我们需要密钥和随机数。密钥位于 Google Chrome 文件中,随机数位于加密的 cookie 值中。
我仍然不清楚 - 是什么决定使用哪种方法
对于正在寻找代码的人,我正在扩展 Cerberus 的答案。 Chrome80版本开始,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 位。加密消息格式为,pay load('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);
}
}
需要的包