资讯动态

仿谷歌身份验证器源码解析:TOTP算法与Android实现

发布时间:2026/10/26 2:11:11 来源:尧图企业网站定制
简介这份资源是面向Android开发者与Java学习者的仿谷歌身份验证器开源项目源码基于Java 8实现可用于理解TOTP动态口令的完整落地流程。项目围绕固定秘钥与实时时间点生成6位验证码、每30秒刷新一次的核心机制展开覆盖秘钥生成与保存、网页与后台及APP三方验证码比对等关键环节适合需要为自有系统接入二次验证、或研究身份认证原理的中级开发者参考。压缩包共114个文件约5.72MB以35个java源文件与31个xml布局配置为主体辅以26个png界面素材、3个apk成品包及gradle构建脚本、jar依赖等结构完整、可直接导入Android Studio运行调试。已有442人学习下载读者可从中获取验证码生成算法实现、秘钥管理思路与前后端校验流程并借助现成APK快速验证效果是理解谷歌身份验证器工作原理的实用参考。1. 从一块“会跳数字的屏幕”说起这个仿谷歌身份验证器到底能干什么第一次拿到这个项目时我盯着手机屏幕上每 30 秒跳一次的六位数字看了半天——它不是在联网拉数据而是纯靠本地时间和一把密钥算出来的。这就是仿谷歌身份验证器的核心TOTP基于时间的一次性密码算法。项目用 Java 8 写成跑在 Android 上源码完整可编译适合想搞懂双因素认证2FA底层逻辑、或者要给自家 App 加一层动态口令的开发者。它不依赖任何第三方认证服务密钥生成、二维码扫描、倒计时刷新、口令校验全在本地闭环拿来当学习样本或者二次开发底座都合适。系统开源代码结构清晰新手能照着跑通熟手能直接拆算法模块。2. 项目骨架与技术选型Java 8 Android 原生没有花架子2.1 为什么是 Java 8 而不是 Kotlin这个项目诞生时间偏早选 Java 8 是那个阶段 Android 开发的主流做法。放到今天看Java 8 的 lambda 和 Stream 在密钥处理、字节数组转换里够用而且源码里大量byte[]操作、Base32编解码、HMAC-SHA1调用用 Java 写反而比 Kotlin 的语法糖更直白——你一眼能看出每一步在动哪块内存。项目没有引入 Retrofit、RxJava 这类网络/异步框架因为 TOTP 根本不需要联网所有计算都在System.currentTimeMillis()的基础上完成。常见做法是用java.util.concurrent起一个定时任务刷新倒计时项目里也是这么干的。技术栈清单大致如下层面选型说明语言Java 8源码级别兼容老 Android 版本平台Android 原生 SDK无跨平台框架加密JCE 内置Mac/SecretKeySpecHMAC-SHA1编码自实现 Base32谷歌验证器密钥标准UI原生 View 定时器倒计时进度条 六位数字2.2 目录结构长什么样源码包解压后是标准 Android 工程结构核心逻辑集中在app/src/main/java下。我按模块拆一下GoogleAuth_Android/ ├── app/ │ ├── src/main/ │ │ ├── java/com/example/googleauth/ │ │ │ ├── MainActivity.java // 主界面展示口令与倒计时 │ │ │ ├── TotpGenerator.java // TOTP 核心算法 │ │ │ ├── Base32.java // Base32 编解码 │ │ │ └── KeyStoreHelper.java // 密钥本地存储 │ │ ├── res/layout/activity_main.xml // 界面布局 │ │ └── AndroidManifest.xml │ └── build.gradle // 模块依赖 └── build.gradle // 工程级配置TotpGenerator是整个项目的心脏Base32负责把用户输入的密钥字符串转成字节数组KeyStoreHelper用 Android 的SharedPreferences或KeyStore做本地持久化。没有多余模块读起来不费劲。2.3 跑起来之前要确认的三件事第一Android Studio 版本别太老Gradle 插件和 Java 8 的兼容性要匹配建议用 Arctic Fox 之后的版本。第二build.gradle里compileSdkVersion和minSdkVersion按你本地 SDK 调整项目默认值可能偏低。第三真机或模拟器的时间必须准——TOTP 对时间偏差极其敏感差 30 秒以上口令就对不上这是后面避坑章节要重点说的。# 克隆或解压后进入工程根目录 cd GoogleAuth_Android # 用 Gradle 包装器编译Windows 用 gradlew.bat ./gradlew assembleDebug # 安装到已连接设备 ./gradlew installDebugassembleDebug产出 APK 在app/build/outputs/apk/debug/下installDebug直接推到设备。如果卡在依赖下载检查build.gradle里的仓库地址是否可达换成国内镜像即可。3. 核心算法拆解一个六位数字是怎么算出来的3.1 TOTP 的时间窗口与计数器TOTP 的本质是把“当前时间”切成固定长度的窗口每个窗口算一个 HMAC 值再截断成六位数字。谷歌验证器默认窗口 30 秒项目里用常量TIME_STEP 30表示。计数器counter 当前Unix秒 / 30这个整数就是 HMAC 输入的一部分。// TotpGenerator.java 片段 private static final int TIME_STEP 30; // 时间窗口单位秒 private static final int DIGITS 6; // 输出位数 public static String generate(String secret) { long unixTime System.currentTimeMillis() / 1000L; long counter unixTime / TIME_STEP; // 当前时间窗口编号 byte[] key Base32.decode(secret); // 密钥从 Base32 还原 byte[] data new byte[8]; for (int i 7; i 0; i--) { // 计数器转 8 字节大端 data[i] (byte) (counter 0xFF); counter 8; } byte[] hash hmacSha1(key, data); // HMAC-SHA1 int offset hash[hash.length - 1] 0x0F; // 动态截断偏移 int binary ((hash[offset] 0x7F) 24) | ((hash[offset 1] 0xFF) 16) | ((hash[offset 2] 0xFF) 8) | (hash[offset 3] 0xFF); int otp binary % (int) Math.pow(10, DIGITS); return String.format(%0 DIGITS d, otp); }这段代码有几个关键点。counter是 8 字节大端序循环里从高位往低位填别写反了否则算出来的口令和谷歌验证器对不上。offset取哈希最后一位的低 4 位这是 RFC 6238 规定的动态截断范围 0~15。binary取四个字节但最高位清零 0x7F避免符号位干扰。最后对 10 的 6 次方取模补前导零。参数secret是 Base32 字符串长度一般 16 或 32 字符解码后作为 HMAC 密钥。3.2 HMAC-SHA1 的调用细节项目没有自己实现 SHA1直接用 JCE 的Mac类。这里容易翻车的是SecretKeySpec的算法名必须写HmacSHA1大小写敏感。private static byte[] hmacSha1(byte[] key, byte[] data) { try { Mac mac Mac.getInstance(HmacSHA1); SecretKeySpec spec new SecretKeySpec(key, HmacSHA1); mac.init(spec); return mac.doFinal(data); } catch (Exception e) { throw new RuntimeException(HMAC 计算失败, e); } }Mac.getInstance在 Android 各版本上都可用不需要额外依赖。doFinal返回 20 字节哈希。如果这里抛NoSuchAlgorithmException多半是混淆规则把 JCE 类裁掉了检查proguard-rules.pro是否保留javax.crypto.**。3.3 Base32 解码谷歌密钥的“翻译官”谷歌验证器导出的密钥是 Base32 编码字母 A-Z 和 2-7不是常见的 Base64。项目里Base32.decode把字符串按 5 位一组还原成字节。核心逻辑是查表加位拼接private static final String ALPHABET ABCDEFGHIJKLMNOPQRSTUVWXYZ234567; public static byte[] decode(String input) { input input.replace(, ).toUpperCase(); // 去填充、统一大写 int buffer 0, bitsLeft 0; ByteArrayOutputStream out new ByteArrayOutputStream(); for (char c : input.toCharArray()) { int val ALPHABET.indexOf(c); if (val 0) throw new IllegalArgumentException(非法 Base32 字符: c); buffer (buffer 5) | val; // 每次塞 5 位 bitsLeft 5; if (bitsLeft 8) { out.write((buffer (bitsLeft - 8)) 0xFF); bitsLeft - 8; } } return out.toByteArray(); }buffer累积位bitsLeft记录当前攒了多少位满 8 位就吐一个字节。注意输入里的填充要剥掉大小写要统一否则indexOf返回 -1 直接抛异常。这个实现没有做严格的长度校验二次开发时建议补上“长度必须是 8 的倍数去填充后”的检查。3.4 倒计时刷新与界面联动MainActivity里用一个Handler或Timer每 500 毫秒刷新一次进度条和剩余秒数到 30 秒边界时重新调用generate。刷新频率比 1 秒高是为了进度条动画顺滑但口令只在窗口切换时重算避免无谓的 HMAC 开销。private final Handler handler new Handler(Looper.getMainLooper()); private final Runnable tick new Runnable() { Override public void run() { long remain TIME_STEP - (System.currentTimeMillis() / 1000L) % TIME_STEP; progressBar.setProgress((int) remain); if (remain TIME_STEP) { // 新窗口重算口令 tvCode.setText(TotpGenerator.generate(secret)); } handler.postDelayed(this, 500); } };remain算的是当前窗口还剩几秒等于TIME_STEP说明刚跨进新窗口。postDelayed500 毫秒递归调度onDestroy里记得handler.removeCallbacks(tick)不然 Activity 销毁后还在跑内存泄漏的血泪经验。4. 避坑与排查这五个坑我踩过你别再踩4.1 口令永远对不上差一位或完全错现象生成的六位数字和谷歌验证器 App 显示的不一致有时差一位有时完全乱。原因通常有两个——设备时间不准或者 Base32 解码时大小写/填充没处理干净。解决先开系统“自动设置时间”再在代码里打印counter和hash前几个字节和在线 TOTP 计算器比对。如果counter一致但哈希不同检查密钥字符串是否被trim()掉了尾部空格。4.2 编译报Duplicate class或 JCE 相关冲突现象assembleDebug时报javax.crypto重复类。原因某些第三方库自带 BouncyCastle和 Android 内置 JCE 打架。解决在build.gradle里排除冲突模块或者干脆不引入任何加密第三方库——这个项目本身就不需要。4.3 模拟器时间漂移导致倒计时跳变现象模拟器挂起再恢复后进度条突然跳一大截口令窗口错乱。原因模拟器休眠时System.currentTimeMillis()照走但 UI 线程被挂起。解决在onResume里强制重算一次口令并重置进度条别依赖挂起前的状态。4.4 密钥存 SharedPreferences 明文安全审计过不了现象二次开发上线前安全扫描报“敏感信息明文存储”。原因KeyStoreHelper早期版本直接putString。解决改用 AndroidKeyStore生成 AES 密钥加密后再存或者至少用EncryptedSharedPreferences。这是改造章节会展开的点。4.5 混淆后 release 包口令计算崩溃现象debug 正常release 包一算口令就RuntimeException。原因ProGuard 把Mac、SecretKeySpec相关类裁了或改了名。解决在proguard-rules.pro加-keep class javax.crypto.** { *; }和-keep class java.security.** { *; }重新打包验证。5. 二次开发改造从“能跑”到“能上线”的三步走5.1 密钥安全把明文存储换成 KeyStore 加密原项目用SharedPreferences存 Base32 密钥学习够用上线不够。我一般会加一层KeyStoreHelper改造用 AndroidKeyStore生成一个 AES 密钥AndroidKeyStoreprovider加密后再写入SharedPreferences。读取时先解密再传给TotpGenerator。这样即使设备被 root密钥也不是明文躺在 XML 里。改造时注意KeyStore在 API 23 以下行为不一致minSdkVersion低于 23 要做降级分支。5.2 多账户支持把单密钥改成列表原项目只维护一个secret变量。要支持多账户把SharedPreferences的存储结构改成 JSON 数组每个元素含{name, secret, issuer}主界面用RecyclerView展示每个 item 独立倒计时。TotpGenerator.generate本身无状态直接复用不用改算法。验证方法添加两个不同密钥的账户对比各自口令与谷歌验证器是否一致。5.3 校验容错窗口允许前后一个时间步实际登录场景里用户输入口令时可能刚好跨窗口严格只认当前窗口会误杀。常见做法是校验时同时算counter-1、counter、counter1三个值任一匹配即通过。改TotpGenerator加一个verify(secret, code, window)方法window默认 1。注意别把窗口开太大前后各 1 步共 90 秒是业界常见容忍度再大就削弱安全性了。改造项改动文件验证方式KeyStore 加密KeyStoreHelper.java反编译查 XML 无明文多账户MainActivity 存储层双账户口令比对容错校验TotpGenerator.java跨窗口输入能通过改造完记得跑一遍 release 混淆包别只测 debug。从那以后我每次动加密相关代码都强制走一遍“debug 跑通 → release 混淆 → 真机跨窗口校验”三步少一步都可能在上线前夜翻车。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑