mcpField
M
maui-secure-storage

maui-secure-storage

github.com/davidortinau安全17653 次安装16 周前更新
代码仓库

关于 maui-secure-storage

面向 .NET MAUI 开发者的安全存储指南,讲解 Android、iOS 和 Windows 上的风险与实践。帮助安全处理机密、考虑平台行为,并通过依赖注入让存储访问可测试。

主要功能

  1. 01平台特定的存储风险
  2. 02Android 备份与异常处理
  3. 03iOS Keychain 行为指南
  4. 04Windows 存储限制
  5. 05依赖注入测试模式
  6. 06机密处理检查清单

适用场景

  1. 01在 MAUI 应用中存储令牌和密码
  2. 02防止 Android 备份数据导致读取失败
  3. 03规划重装后仍保留的 Keychain 数据
  4. 04让 ViewModel 存储访问可模拟测试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/davidortinau/maui-skills --skill maui-secure-storage

安装后在 Claude Code 中输入/maui-secure-storage确认已加载。

附带文件references/secure-storage-api.md

详细介绍

安全存储 — 注意事项与最佳实践

关键平台陷阱

⚠️ Android:自动备份会破坏加密值

自动备份会将加密后的偏好设置恢复到新设备,而加密密钥在新设备上无效——这会引发无法恢复的异常。你必须禁用自动备份,或将安全存储文件排除在备份之外。设置选项请参阅 references/secure-storage-api.md。

⚠️ Android:务必使用 try/catch 包裹

备份恢复产生的损坏值会引发异常。切勿在未受保护的情况下调用 GetAsync:

// ❌ Unprotected — crashes on corrupted backup data
var value = await SecureStorage.Default.GetAsync("key");

// ✅ Protected — handles corruption gracefully
try
{
    var value = await SecureStorage.Default.GetAsync("key");
}
catch (Exception)
{
    SecureStorage.Default.RemoveAll();
}
⚠️ iOS:模拟器上的钥匙串权限

为模拟器构建添加钥匙串访问组,但在实体设备 / App Store 构建前务必移除——在不需要这些权限的设备上,它们会导致签名问题。

⚠️ iOS:卸载不会清除钥匙串

与 Android 不同,卸载 iOS 应用不会移除其钥匙串条目。值会保留,并在应用重新安装后继续可用。请据此进行设计——不要假设全新安装意味着存储为空。

⚠️ iOS:iCloud 钥匙串同步

如果用户启用了 iCloud 钥匙串,值可能会通过它在设备间同步。这是平台行为,无法通过 MAUI 控制。不要存储不应在设备间漫游的设备专属令牌。

Windows 限制
  • 键名:最多 255 个字符
  • 值:每项最多 8 KB
  • 复合存储:总计最多 64 KB

常见错误

// ❌ Storing large data — SecureStorage is for small secrets only
await SecureStorage.Default.SetAsync("profile_image", base64EncodedImage);

// ✅ Store tokens, passwords, short secrets
await SecureStorage.Default.SetAsync("auth_token", jwtToken);

// ❌ Logging secret values
_logger.LogInformation("Token: {Token}", await SecureStorage.Default.GetAsync("auth_token"));

// ✅ Log existence, not value
_logger.LogInformation("Token exists: {Exists}", token is not null);

// ❌ Storing complex objects without serialization (values are strings only)
await SecureStorage.Default.SetAsync("user", userObject);

// ✅ Serialize to JSON first
await SecureStorage.Default.SetAsync("user", JsonSerializer.Serialize(user));

决策框架

问题答案
要存储令牌、密码或 API 密钥?✅ 使用 SecureStorage
要存储用户偏好设置或配置?❌ 改用 Preferences
要存储大型文件或二进制数据?❌ 使用文件系统 + 加密
需要跨设备同步?⚠️ iOS 会通过 iCloud 钥匙串自动同步
需要在卸载时清除数据?⚠️ 仅适用于 Android,不适用于 iOS

可测试性:始终使用 DI

切勿在 ViewModel 中直接调用 SecureStorage.Default——请将其封装在 ISecureStorageService 接口中,以便进行测试。包含模拟示例的完整 DI 封装模式请参阅 references/secure-storage-api.md。

// ❌ Direct static access — untestable
public class LoginViewModel
{
    public async Task SaveToken(string token)
        => await SecureStorage.Default.SetAsync("auth_token", token);
}

// ✅ Inject interface — testable and mockable
public class LoginViewModel(ISecureStorageService secure)
{
    public async Task SaveToken(string token)
        => await secure.SetAsync("auth_token", token);
}

检查清单

  • Android:已禁用自动备份,或已将安全存储排除在备份之外
  • Android:所有 GetAsync 调用均使用 try/catch 包裹
  • iOS:已配置钥匙串权限(仅限模拟器——设备构建前移除)
  • 值仅为字符串——复杂数据已序列化为 JSON
  • 未在任何位置记录机密值
  • 通过 DI 封装访问 SecureStorage.Default,而非在 ViewModel 中直接访问

评论

支持 Markdown