乐于分享
好东西不私藏

密码管理器 2password 安卓实现

密码管理器 2password 安卓实现

Android 密码管理器:安全存储与 MFA 实战

之前一篇文章《从1password到自建密码管理器:2password实战指南》,介绍了用 PyQt5 + MySQL 构建桌面版密码管理器的完整过程。桌面版运行稳定,但在实际使用中,手机端使用也很频繁。

于是我开始着手 2password 的 Android 版本,目标是实现与桌面版同等安全级别的移动端密码管理方案。

相比桌面版,Android 版密码管理器有其独特的优势:

  • 硬件级安全
    :利用 Android KeyStore 实现硬件级密钥保护,这是桌面端难以实现的
  • 生物识别集成
    :原生支持指纹和面容识别,解锁更便捷
  • 系统级自动锁定
    :利用应用生命周期实现自动锁定,安全无死角
  • 现代 UI 框架
    :Jetpack Compose 提供声明式 UI 开发体验,代码更简洁

今天这篇文章,我将分享如何构建一个功能完善的 Android 密码管理器。如果你读过我的 2password 文章,会发现这两个项目在设计理念上有很多相似之处,但在技术实现上又有各自的特点。先看看效果!

      

一、为什么从桌面版到移动版?

在开始之前,先聊聊为什么我要从桌面版的 2password 扩展到 Android 版本。

在《2password 实战指南》那篇文章中,我分享了一个基于 PyQt5 + MySQL 的桌面密码管理器。实际使用后,我发现了一些痛点:

  • 移动场景需求
    :很多时候我在手机上浏览网页,需要快速复制密码,但桌面版无法直接使用
  • 安全性限制
    :桌面版的密码存储在本地 MySQL,虽然实用但缺乏硬件级密钥保护
  • 生物识别缺失
    :桌面版没有原生生物识别支持,每次都要输入主密码略显繁琐

选择 Android 平台作为移动端方案,主要基于以下考虑:

  • 生态成熟
    :Jetpack Compose 提供现代化的 UI 开发体验,代码量比 PyQt5 更少
  • 安全框架
    :Android KeyStore 提供硬件级密钥保护,这是桌面端难以实现的安全特性
  • 生物识别
    :原生支持指纹和面容识别,解锁更便捷
  • 学习价值
    :从桌面到移动,是一次很好的技术栈拓展

两个版本的设计理念是一致的:简单实用、安全第一、数据自主。但在技术实现上,Android 版充分利用了平台特性,带来了更好的安全性和使用体验。

二、整体架构设计

密码管理器采用 MVVM 架构,结合 Jetpack Compose 构建 UI。整体架构如下:

三、核心功能模块

密码管理器的功能围绕”安全、便捷、可追溯”的原则设计。每个功能模块都经过精心打磨,确保既满足日常使用需求,又不牺牲安全性。

四、密码添加流程

密码添加是一个涉及验证、加密、存储的完整流程。看似简单的”添加密码”操作,背后其实有一套严密的安全机制在运行。

五、数据库设计

使用 Room 数据库,设计了两个核心表:密码表和历史表。每个密码都对应一个历史记录集合,这种设计确保了数据的完整性和可追溯性。

 

六、安全加密实现

密码安全是核心功能。使用 Android KeyStore 生成并存储密钥,配合 AES-256-GCM 算法加密数据。这里有几个关键的安全细节值得注意。

object EncryptionManager {
privateconst val KEY_ALIAS = "password_manager_key"
privateconst val KEYSTORE_PROVIDER = "AndroidKeyStore"
privateconst val TRANSFORMATION = "AES/GCM/NoPadding"

private val keyStore: KeyStore by lazy {
        KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) }
    }

private fun createKey(): SecretKey {
        val keyGenerator = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES,
            KEYSTORE_PROVIDER
        )

        val spec = KeyGenParameterSpec.Builder(KEY_ALIAS,
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .setKeySize(256)
            .setRandomizedEncryptionRequired(true)  // 每次加密使用随机IV
            .build()

return keyGenerator.generateKey()
    }

    fun encrypt(data: String): EncryptedData {
        val cipher = Cipher.getInstance(TRANSFORMATION)
        cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())

        val encryptedBytes = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
        val iv = cipher.iv

return EncryptedData(
            ciphertext = Base64.encodeToString(encryptedBytes, Base64.NO_WRAP),
            iv = Base64.encodeToString(iv, Base64.NO_WRAP)
        )
    }
}

七、生物识别与自动锁定

为了提升安全性和便捷性,应用集成了生物识别和自动锁定功能:

@Singleton
classBiometricAuthManager @Injectconstructor() {

enumclassBiometricStatus {
        AVAILABLE,
        NO_HARDWARE,
        HARDWARE_UNAVAILABLE,
        NONE_ENROLLED,
        SECURITY_UPDATE_REQUIRED
    }

    fun checkBiometricAvailability(context: Context): BiometricStatus {
        val biometricManager = BiometricManager.from(context)
returnwhen (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
            BiometricManager.BIOMETRIC_SUCCESS -> BiometricStatus.AVAILABLE
            BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> BiometricStatus.NO_HARDWARE
            BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> BiometricStatus.HARDWARE_UNAVAILABLE
            BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> BiometricStatus.NONE_ENROLLED
else -> BiometricStatus.NO_HARDWARE
        }
    }

    suspend fun authenticate(
activity: FragmentActivity,
title: String = "验证身份",
subtitle: String = "使用生物识别解锁密码管理器"
    ): Result<Boolean> = suspendCancellableCoroutine { continuation ->
        val biometricPrompt = BiometricPrompt(
            activity,
            ContextCompat.getMainExecutor(activity),
            object : BiometricPrompt.AuthenticationCallback() {
                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                    continuation.resume(Result.success(true))
                }

                override fun onAuthenticationFailed() {
                    continuation.resume(Result.success(false))
                }

                override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                    continuation.resume(Result.failure(BiometricAuthException(errorCode, errString.toString())))
                }
            }
        )

        val promptInfo = BiometricPrompt.PromptInfo.Builder()
            .setTitle(title)
            .setSubtitle(subtitle)
            .setNegativeButtonText("取消")
            .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
            .build()

        biometricPrompt.authenticate(promptInfo)
    }
}

八、MFA 验证码生成

支持 TOTP (Time-based One-Time Password) 算法,与 Google Authenticator 等应用完全兼容。这意味着你可以用一个密码管理器同时管理密码和 MFA,无需多个应用切换。

TOTP 的工作原理

TOTP 基于时间生成一次性密码,每 30 秒变化一次。服务端和客户端使用相同的密钥和当前时间计算验证码,只要时间同步,就能产生相同的验证码。RFC 6238 是这一算法的标准规范。

object TotpGenerator {
privateconst val HMAC_SHA1 = "HmacSHA1"
privateconst val DEFAULT_DIGITS = 6
privateconst val DEFAULT_PERIOD = 30

    fun generateTotpCode(secret: String, digits: Int = DEFAULT_DIGITS): String {
if (secret.isEmpty()) return""

        val key = decodeBase32(secret)
        val time = System.currentTimeMillis() / 1000 / DEFAULT_PERIOD
return generateOtp(key, time, digits)
    }

private fun generateOtp(key: ByteArray, time: Long, digits: Int): String {
        val data = ByteBuffer.allocate(8).putLong(time).array()
        val signKey = SecretKeySpec(key, HMAC_SHA1)
        val mac = Mac.getInstance(HMAC_SHA1)
        mac.init(signKey)
        val hash = mac.doFinal(data)

        val offset = hash[hash.size - 1].toInt() and0xf
        val truncatedHash = ((hash[offset].toInt() and0x7f) shl 24) or
                           ((hash[offset + 1].toInt() and0xff) shl 16) or
                           ((hash[offset + 2].toInt() and0xff) shl 8) or
                           (hash[offset + 3].toInt() and0xff)

        val otp = truncatedHash % (10.0.pow(digits.toDouble()).toInt())
return otp.toString().padStart(digits, '0')
    }
}

九、历史记录追踪

所有密码修改都会自动记录历史,支持查看和恢复旧值。历史记录采用不可删除设计,确保审计完整性:

十、数据导入导出

 支持加密格式的数据导入导出,方便在设备间迁移,防止删除应用时数据丢失。

备份的重要性

再好的软件也可能出问题,手机也可能丢失或损坏。定期备份数据是保护自己资产的重要习惯。导出的数据文件也是加密的,即使文件被窃取也无法读取内容。

十一、主题定制

为了提供个性化的使用体验,应用支持火主题:

private val LightColorScheme = lightColorScheme(
    primary = Color(0xFFD32F2F),           // 火主色
    primaryContainer = Color(0xFFFFE0E0),   // 浅红色容器
    secondary = Color(0xFFFF5722),         // 橙红色辅助色
    secondaryContainer = Color(0xFFFFCCBC), // 橙色容器
    tertiary = Color(0xFFE64A19),          // 深红色第三色
    background = Color(0xFFFFFBFE),        // 背景色
    surface = Color(0xFFFFFBFE),           // 表面色
    onPrimary = Color.White,               // 主色文字
    onSecondary = Color.White,             // 辅助色文字
    onTertiary = Color.White,             // 第三色文字
    onBackground = Color(0xFF1C1B1F),      // 背景文字
    onSurface = Color(0xFF1C1B1F)         // 表面文字
)

@Composable
fun PasswordManagerTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = false,  // 禁用 Material You
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColorScheme
else -> LightColorScheme
    }

    MaterialTheme(
        colorScheme = colorScheme,
        content = content
    )
}

关键点:设置 dynamicColor = false 禁用 Material You 动态颜色,确保自定义主题生效。

十二、安全最佳实践

通过这个项目,我总结了一些移动端密码管理器开发的最佳实践。这些经验不仅适用于密码管理器,也适用于任何需要处理敏感数据的 Android 应用:

安全维度
推荐做法
本项目实现
密钥存储
使用 Android KeyStore 硬件保护
✅ KeyStore + AES-256-GCM
加密算法
AES-256-GCM(认证加密)
✅ GCM 模式 + IV 随机化
数据传输
禁用应用备份
✅ allowBackup=false
访问控制
生物识别 + 自动锁定
✅ BiometricAuthManager
数据完整性
历史记录不可删除
✅ 历史表无删除接口
MFA 保护
MFA 密钥加密存储
✅ mfaSecret 字段加密
数据备份
提供导出备份功能
✅ 加密格式导出/导入
主题保护
禁用动态颜色覆盖
✅ dynamicColor=false

十三、Android 版 vs 桌面版对比

经过这次实践,我想对比一下两个版本的差异,帮助大家根据自己的需求选择合适的方案: 

特性
2password (桌面版)
Android 版
技术栈
Python + PyQt5 + MySQL
Kotlin + Jetpack Compose + Room
数据存储
本地 MySQL 数据库
Room SQLite 数据库
加密方式
cryptography (AES-256)
Android KeyStore + AES-256-GCM
生物识别
❌ 无原生支持
✅ 指纹/面容识别
自动锁定
✅ 超时锁定
✅ 后台自动锁定
MFA 支持
✅ TOTP 生成
✅ TOTP 生成
历史记录
✅ 不可删除
✅ 不可删除
数据迁移
✅ CSV 导入导出
✅ 加密格式导入导出
安全性
⭐⭐⭐⭐
⭐⭐⭐⭐⭐ (硬件级保护)
便捷性
⭐⭐⭐⭐
⭐⭐⭐⭐⭐ (生物识别)

我的使用体验

  • 桌面版适合:在电脑前长时间工作,需要批量管理密码的场景
  • Android 版适合:移动场景下的快速查看和复制密码,随时随地可用
  • 两个版本的数据是独立的,我目前同时使用,数据通过加密备份文件手动同步

十四、项目总结

这个自建的密码管理器完全满足我的日常需求:

  • – 本地存储,数据完全自主可控
  • – 硬件级加密,安全性有保障
  • – MFA 支持,与所有 TOTP 应用兼容
  • – 历史记录,所有操作可追溯
  • – 数据备份,防止删除应用时数据丢失
  • – 生物识别,便捷安全的解锁方式
  • – 自动锁定,保护应用免受未授权访问

如果你也在考虑自建密码管理器,不妨从 Android 平台开始。这是一个实用性强的项目,既能学到知识,又能解决实际问题。

最后提醒:密码安全无小事。无论使用哪种密码管理器,请确保:

使用强密码(长度至少 12 位,包含大小写、数字、特殊字符)

  • 为每个网站使用不同的密码
  • 启用 MFA 双因素认证
  • 定期备份数据
  • 不要将密码分享给他人

相关阅读


作者:王梓 | 葫芦的运维日志
原文链接:https://www.bthlt.com/note/369883662
转载请注明出处