[TOTP]android kotlin实现 totp身份验证器 类似Google身份验证器
背景:自己或者公司用一些谷歌身份验证器或者microsoft身份验证器,下载来源不明,或者有广告,使用不安全。于是自己写一个,安全放心使用。
代码已开源:shixiaotian/sxt-android-totp: android totp authenticator (github.com)
效果图
此身份验证器,一共1个activity,3个fragment。
实现原理,通过线程动态触发totp算法,从加密的sqlite里读取密钥信息进行加密计算。新增密钥可通过扫描二维码,或者手动添加的方式实现。
一.添加对应的包
包括,totp算法包,zxing二维码扫描包,sqlite加密包
分别为
authenticator="1.0.0"
zxing-android-embedded="4.2.0"
database-sqlcipher="4.5.0"
androidx-authenticator = { group = "org.jboss.aerogear", name = "aerogear-otp-java", version.ref = "authenticator" }
androidx-zxing-android-embedded = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxing-android-embedded" }
androidx-database-sqlcipher ={ group = "net.zetetic", name = "android-database-sqlcipher", version.ref = "database-sqlcipher" }
文件
libs.versions.toml
[versions]
agp = "8.7.2"
kotlin = "1.9.24"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
appcompat = "1.6.1"
material = "1.10.0"
activity = "1.8.0"
constraintlayout = "2.1.4"
authenticator="1.0.0"
zxing-android-embedded="4.2.0"
database-sqlcipher="4.5.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-authenticator = { group = "org.jboss.aerogear", name = "aerogear-otp-java", version.ref = "authenticator" }
androidx-zxing-android-embedded = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxing-android-embedded" }
androidx-database-sqlcipher ={ group = "net.zetetic", name = "android-database-sqlcipher", version.ref = "database-sqlcipher" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
build.gradele.kts
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.shixiaotian.totp.scan.application"
compileSdk = 34
defaultConfig {
applicationId = "com.shixiaotian.totp.scan.application"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.authenticator)
implementation(libs.androidx.espresso.core)
implementation(libs.androidx.zxing.android.embedded)
implementation(libs.androidx.database.sqlcipher)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
二.项目程序文件结构为
三.全部程序文件
MyConstant
作用:常量类,添加了如下三个参数,方便自行变换,增加安全性和防止撞库
package com.shixiaotian.totp.scan.application.common
class MyConstants {
companion object {
//数据库名称,记得加扰动防止重复
const val dbName = "sxt.auth.code.0098675.db"
// 数据库密码
const val dbPassword = "jsdfjhkldsvcbuehuisudbekokmhshyebgqoondyasd"
// app 首次运行标记
const val firstRunTag = "sxt.auth.code.0098674.firstRunTag"
}
}
DatabaseHelper
作用:数据库操作相关类,集成了数据库加密,初始化,基本插入、读取、删除等功能
package com.shixiaotian.totp.scan.application.db
import android.content.Context
import android.database.Cursor
import androidx.core.content.contentValuesOf
import com.shixiaotian.totp.scan.application.common.MyConstants
import com.shixiaotian.totp.scan.application.vo.User
import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SQLiteOpenHelper
class DatabaseHelper(context: Context) : SQLiteOpenHelper(context, MyConstants.dbName, null, 1) {
private val dbContext:Context = context
override fun onCreate(db: SQLiteDatabase) {
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
// 更新数据库的时候调用
}
private fun openEncryptedDatabase(): SQLiteDatabase {
val dbHelper = DatabaseHelper(dbContext)
val db = dbHelper.getWritableDatabase(MyConstants.dbPassword)
return db
}
// 写入数据
fun insertUser(username: String, secretKey: String, issuer: String): Long {
val db = this.openEncryptedDatabase()
var id =db.insert("sxt_totp_users", null, contentValuesOf("username" to username, "secretKey" to secretKey, "issuer" to issuer))
db.close()
return id
}
// 删除数据
fun deleteUser(id: Int) {
val db = this.openEncryptedDatabase()
db.delete("sxt_totp_users", "id = ?", arrayOf(id.toString()))
db.close()
}
// 查询数据
fun getUser(id: Int): User? {
val db = this.openEncryptedDatabase()
val cursor: Cursor = db.query("sxt_totp_users", null, "id = ?", arrayOf(id.toString()), null, null, null)
var user: User? = null
if (cursor.moveToFirst()) {
val id = cursor.getInt(0)
val name = cursor.getString(1)
val secretKey = cursor.getString(2)
val issuer = cursor.getString(3)
// 假设User有id和name两个字段
user = User(id, name,secretKey, issuer)
}
cursor.close()
db.close()
return user
}
fun getAllUser(): List<User> {
val db = this.openEncryptedDatabase()
val items = ArrayList<User>()
val cursor: Cursor = db.query("sxt_totp_users", arrayOf("id","username","secretKey", "issuer"), null, null, null, null, null)
cursor.moveToFirst()
while (!cursor.isAfterLast) {
val id = cursor.getInt(0)
val name = cursor.getString(1)
val secretKey = cursor.getString(2)
val issuer = cursor.getString(3)
items.add(User(id, name,secretKey, issuer))
cursor.moveToNext()
}
cursor.close()
db.close()
return items
}
fun initDB() {
// 创建表
val db = this.openEncryptedDatabase()
db.execSQL("CREATE TABLE sxt_totp_users (id INTEGER PRIMARY KEY, username TEXT, secretKey TEXT, issuer TEXT)")
db.close()
}
fun init() {
this.insertUser("apple","apple", "github")
this.insertUser("pear","pear", "steam")
this.insertUser("apricot","apricot","wiki")
this.insertUser("peach","peach","TK")
}
}
CodeAddFragment
作用:令牌添加页面片段,提供手动输入令牌,和触发zxing令牌扫描功能,并回收zxing扫描后的结果,进行存储,将相关id数据传输给codeShow单独展示的页面进行展示。
package com.shixiaotian.totp.scan.application.fragments
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import com.shixiaotian.totp.scan.application.db.DatabaseHelper
import com.shixiaotian.totp.scan.application.tools.EncodeTools
import com.google.zxing.integration.android.IntentIntegrator
import com.journeyapps.barcodescanner.CaptureActivity
import com.shixiaotian.totp.scan.application.R
// TODO: Rename parameter arguments, choose names that match
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
/**
* A simple [Fragment] subclass.
* Use the [CodeAddFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class CodeAddFragment : Fragment() {
private lateinit var saveButton: View
private lateinit var scanButton: View
private lateinit var nameText: TextView
private lateinit var secretKeyText: TextView
private lateinit var issuerText: TextView
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
@SuppressLint("MissingInflatedId")
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_code_add, container, false)
val dbHelper = DatabaseHelper(requireContext())
saveButton = view.findViewById(R.id.saveButton)
saveButton.setOnClickListener {
nameText = view.findViewById<TextView>(R.id.addUsernameText)
val name = nameText.getText();
secretKeyText = view.findViewById<TextView>(R.id.addSecretKeyText)
val secretKey = secretKeyText.getText();
issuerText = view.findViewById<TextView>(R.id.addIssuerText)
val issuer = issuerText.getText();
if(name.isEmpty()) {
alertAddError("Name can't be blank")
} else if(secretKey.isEmpty()) {
alertAddError("SecretKey can't be blank")
} else if(issuer.isEmpty()){
alertAddError("issuer can't be blank")
} else {
var saveId = dbHelper.insertUser(name.toString(), secretKey.toString(), issuer.toString());
nameText.setText("")
secretKeyText.setText("")
issuerText.setText("")
val fragment = CodeShowFragment.newInstance(saveId.toString(), "")
parentFragmentManager.beginTransaction().replace(R.id.viewPager, fragment).commit()
}
}
scanButton = view.findViewById(R.id.cameraButton)
scanButton.setOnClickListener {
val integrator = IntentIntegrator.forSupportFragment(this)
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
integrator.setOrientationLocked(false)
integrator.captureActivity = CaptureActivity::class.java
integrator.setRequestCode(5766) //_scan为自己定义的请求码
integrator.initiateScan()
}
return view
}
fun alertAddError(msg : String) {
println("alertAddError : " + msg)
val builder = AlertDialog.Builder(context)
builder.setTitle("add error")
builder.setMessage(msg)
builder.setPositiveButton("OK") { dialog, _ ->
dialog.dismiss()
}
builder.create().show()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
//_scan为自己定义的扫码请求码
5766 -> {
// 跳转扫描页面返回扫描数据
var scanResult = IntentIntegrator.parseActivityResult(
IntentIntegrator.REQUEST_CODE,
resultCode,
data
);
// 判断返回值是否为空
if (scanResult != null) {
//返回条形码数据
var result = scanResult.contents
if(result == null) {
parentFragmentManager.beginTransaction().replace(R.id.viewPager, this).commit()
return
}
val user = EncodeTools.decode(result);
if(user == null) {
Toast.makeText(context, "Scan Fail", Toast.LENGTH_SHORT).show()
parentFragmentManager.beginTransaction().replace(R.id.viewPager, this).commit()
return
}
// 保存数据
val dbHelper = DatabaseHelper(requireContext())
var saveId = dbHelper.insertUser(user!!.getUsername(), user.getSecretKey(), user.getIssuer())
if(saveId < 0 ) {
Toast.makeText(context, "Save Data Fail", Toast.LENGTH_SHORT).show()
parentFragmentManager.beginTransaction().replace(R.id.viewPager, this).commit()
return
}
// 跳转
val fragment = CodeShowFragment.newInstance(saveId.toString(), "")
parentFragmentManager.beginTransaction().replace(R.id.viewPager, fragment).commit()
} else {
Toast.makeText(context, "Scan Fail:ERROR", Toast.LENGTH_SHORT).show()
parentFragmentManager.beginTransaction().replace(R.id.viewPager, this).commit()
}
} else -> {
val fragment = CodeAddFragment()
parentFragmentManager.beginTransaction().replace(R.id.viewPager, fragment).commit()
}
}
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment CodeAddFragment.
*/
// TODO: Rename and change types and number of parameters
@JvmStatic
fun newInstance(param1: String, param2: String) =
CodeAddFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
}
CodeListFragment
作用:令牌列表界面,提供了定时器刷新整个列表的功能,和每个令牌单独点击触发的功能
package com.shixiaotian.totp.scan.application.fragments
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Handler
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ListView
import android.widget.TextView
import com.shixiaotian.totp.scan.application.CodeListAdapter
import com.shixiaotian.totp.scan.application.R
import com.shixiaotian.totp.scan.application.db.DatabaseHelper
import com.shixiaotian.totp.scan.application.tools.MyTimeUtils
import com.shixiaotian.totp.scan.application.vo.User
// TODO: Rename parameter arguments, choose names that match
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
/**
* A simple [Fragment] subclass.
* Use the [CodeListFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class CodeListFragment : Fragment() {
private var start: Long = 30
private lateinit var adapter: CodeListAdapter
private val handler = Handler()
private var runnable: Runnable? = null
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
private var userList: List<User>? =null
private val data = listOf("apple","pear","apricot","peach","grape","banana","pineapple","plum","watermelon","orange","lemon","mango","strawberry",
"medlar","mulberry","nectarine","cherry","pomegranate","fig","persimmon")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// 获取视图
val view = inflater.inflate(R.layout.fragment_code_list, container, false)
val listView = view.findViewById<ListView>(R.id.listView)
var textView3 = view.findViewById<TextView>(R.id.textView3)
// 查询数据库
val dbHelper = DatabaseHelper(requireContext())
userList = dbHelper.getAllUser();
//获取当前分钟秒数
// 启动定时器
timer(textView3)
// 创建ArrayAdapter,将数据源传递给它
adapter = CodeListAdapter(requireContext(), R.layout.code_item, userList!!)
// 将适配器与ListView关联
listView.adapter = adapter
listView.setOnItemClickListener { parent, view, position, id ->
val textView = view.findViewById<TextView>(R.id.user_id);
val fragment = CodeShowFragment.newInstance(textView.text.toString(), "")
// 执行跳转
parentFragmentManager.beginTransaction().replace(R.id.viewPager, fragment).commit()
val codeShowFragment = CodeShowFragment()
switchFragment(codeShowFragment)
}
return view
}
// 刷新数据
private fun refresh() {
if(userList != null) {
adapter.notifyDataSetChanged()
}
}
// 定时器
private fun timer(textView : TextView) {
// 动态计算当前秒数
start = MyTimeUtils.getCurrentSec()
runnable = Runnable {
val formattedNumber = String.format("%02d",start/1000)
textView.setText(formattedNumber + "s")
start = start -100
if(start <0) {
refresh()
start= MyTimeUtils.getCurrentSec()
}
// 在这里设置下一次循环的延时时间,例如1秒
handler.postDelayed(runnable!!, 100)
}
// 初始化计时器
handler.postDelayed(runnable!!, 50) // 延时1秒后开始循环
}
@SuppressLint("SuspiciousIndentation")
private fun switchFragment(fragment: Fragment) {
val transaction = parentFragmentManager.beginTransaction()
transaction.show(fragment)
transaction.commit()
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment CodeListFragment.
*/
// TODO: Rename and change types and number of parameters
@JvmStatic
fun newInstance(param1: String, param2: String) =
CodeListFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
override fun onDestroyView() {
println("onDestroyView")
handler.removeCallbacks(runnable!!)
super.onDestroyView()
}
}
CodeShowFragment
作用:令牌单独展示页面的片段代码,用于页面操作功能处理。提供了定时器进行倒计时刷新令牌,和令牌删除功能。
package com.shixiaotian.totp.scan.application.fragments
import android.app.AlertDialog
import android.os.Bundle
import android.os.Handler
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.TextView
import com.shixiaotian.totp.scan.application.R
import com.shixiaotian.totp.scan.application.db.DatabaseHelper
import com.shixiaotian.totp.scan.application.tools.EncodeTools
import com.shixiaotian.totp.scan.application.tools.MyTimeUtils
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
/**
* A simple [Fragment] subclass.
* Use the [CodeShowFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class CodeShowFragment : Fragment() {
private var codeView: TextView? =null
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
private var start: Long = 30000
private val handler = Handler()
private var runnable: Runnable? = null
private var secretKey: String =""
private lateinit var progressBar: ProgressBar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
var id: String? = param1
if(id == null) {
id = "0"
}
// 查询数据库
val dbHelper = DatabaseHelper(requireContext())
val user = dbHelper.getUser(id.toInt())
val view = inflater.inflate(R.layout.fragment_code_show, container, false)
// 初始化进度条
progressBar = view.findViewById<ProgressBar>(R.id.progressBar)
// 设置进度条的最大值
progressBar.max = 30000
// 设置当前进度
progressBar.progress = 30000
// 显示进度条
progressBar.visibility = ProgressBar.VISIBLE
val showIssuerTextView = view.findViewById<TextView>(R.id.showIssuerTextView)
val usernameView = view.findViewById<TextView>(R.id.showUsernameTextView)
codeView = view.findViewById<TextView>(R.id.showCodeView)
val timeView3 = view.findViewById<TextView>(R.id.showTimeView)
if(user != null) {
secretKey = user!!.getSecretKey();
// 开启个线程,动态计算密钥,并更新到ui界面
showIssuerTextView.setText(user.getIssuer())
usernameView.setText(user.getUsername())
if (codeView != null) {
codeView!!.setText(EncodeTools.encode(user.getSecretKey()))
}
timer(timeView3)
}
// 删除按钮
val deleteButton = view.findViewById<TextView>(R.id.deleteButton)
deleteButton.setOnClickListener {
showDeleteConfirmationDialog(id)
}
return view
}
private fun refresh() {
codeView!!.setText(EncodeTools.encode(secretKey))
}
private fun timer(textView : TextView) {
// 动态计算当前秒数
start = MyTimeUtils.getCurrentSec()
runnable = Runnable {
val formattedNumber = String.format("%02d",start/1000)
textView.setText(formattedNumber + "s")
progressBar.setProgress(start.toInt());
start = start -100
if(start < 0) {
start= MyTimeUtils.getCurrentSec()
var refreshRunnable = Runnable {
refresh()
}
Thread(refreshRunnable).start()
}
// 在这里设置下一次循环的延时时间,例如1秒
handler.postDelayed(runnable!!, 100)
}
// 初始化计时器
handler.postDelayed(runnable!!, 50) // 延时1秒后开始循环
}
fun showDeleteConfirmationDialog(deleteId : String) {
val builder = AlertDialog.Builder(context)
builder.setMessage("确定要删除吗?")
.setPositiveButton("Yes") { dialog, id ->
// 删除操作
val dbHelper = DatabaseHelper(requireContext())
dbHelper.deleteUser(deleteId.toInt())
val codeListFragment = CodeListFragment()
parentFragmentManager.beginTransaction().replace(R.id.viewPager, codeListFragment).commit()
}
.setNegativeButton("No") { dialog, id ->
// 取消操作,对话框不会被关闭
}
.setCancelable(false)
val alert = builder.create()
alert.show()
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment CodeShowFragment.
*/
// TODO: Rename and change types and number of parameters
@JvmStatic
fun newInstance(param1: String, param2: String) =
CodeShowFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
override fun onDestroyView() {
if(handler!= null && runnable != null) {
handler.removeCallbacks(runnable!!)
}
super.onDestroyView()
}
}
EncodeTools
作用:软件核心功能,调用jboss包的otp算法,对密钥进行运算,得出动态令牌。解析二维码扫描出的totp链接信息,转换成user实体提供软件运行
package com.shixiaotian.totp.scan.application.tools
import com.shixiaotian.totp.scan.application.vo.User
import org.jboss.aerogear.security.otp.Totp
class EncodeTools {
companion object {
@JvmStatic
fun encode(
secretKey: String,
timeStep: Long = 30,
digits: Int = 6,
algorithm: String = "SHA1"
): String? {
if (secretKey == null || secretKey.isBlank()) {
return ""
}
try {
val totp = Totp(secretKey);
var result = totp.now();
return result;
} catch (e: Exception) {
return "ERROR SK"
}
}
@JvmStatic
fun decode(uri: String): User? {
if(uri.isEmpty()) {
return null
}
if(!uri.startsWith("otpauth://totp/")) {
return null
}
try {
var uriContentIndex = uri.indexOf("otpauth://totp/");
var uriContent = uri.subSequence(15, uri.length);
val secContents = uriContent.split(":");
var issuer = secContents.get(0);
var otherContent = secContents.get(1)
val secOtherContent= otherContent.split("?")
var username = secOtherContent.get(0)
var thOtherContent = secOtherContent.get(1)
val fthOtherContent = thOtherContent.split("&")
var secretKeyContent = fthOtherContent.get(0)
var secretKey = secretKeyContent.split("=").get(1)
var user = User(0, username, secretKey, issuer)
return user
}catch (e: Exception) {
return null
}
}
}
}
FirstRunTools
作用:检测软件是否为第一次安装使用,该段代码不完全适用,建议使用者进行改造,或者移除
package com.shixiaotian.totp.scan.application.tools
import android.content.Context
import android.content.SharedPreferences
import com.shixiaotian.totp.scan.application.common.MyConstants
class FirstRunTools {
companion object {
@JvmStatic fun isFirstRun(context: Context): Boolean {
val prefs: SharedPreferences = context.getSharedPreferences(MyConstants.firstRunTag, Context.MODE_PRIVATE)
val isFirstTime = prefs.getBoolean(MyConstants.firstRunTag + "isFirstTime", true)
if (isFirstTime) {
val editor = prefs.edit()
editor.putBoolean(MyConstants.firstRunTag + "isFirstTime", false)
editor.apply()
return true
}
return false
}
}
}
MyTimeUtils
作用:时间工具,因为totp是每30秒计算一次,而每次进入软件的时间不同,该功能用于纠正进入的时间差,让令牌刷新倒计时进入精确的时间区间。
package com.shixiaotian.totp.scan.application.tools
import android.icu.util.Calendar
class MyTimeUtils {
companion object {
@JvmStatic fun getCurrentSec(): Long {
// 获取当前时间的毫秒数
val currentTimeMillis = System.currentTimeMillis()
// 创建Calendar实例
val calendar = Calendar.getInstance()
// 设置Calendar的时间为当前时间
calendar.timeInMillis = currentTimeMillis
// 将秒和毫秒字段重置为0
calendar.set(Calendar.SECOND, 0)
calendar.set(Calendar.MILLISECOND, 0)
// 当前分钟的开始时间的毫秒数
val startOfCurrentMinuteMillis = calendar.timeInMillis
// 已过去的毫秒数
val elapsedMillis = currentTimeMillis - startOfCurrentMinuteMillis
// 已过去的秒数
//val elapsedSeconds = elapsedMillis / 1000
if(elapsedMillis > 30000) {
return 60000 - elapsedMillis;
} else {
return 30000 - elapsedMillis;
}
}
}
}
User
作用:作为数据传输和存储的实体,存储用户的令牌等相关信息
package com.shixiaotian.totp.scan.application.vo
class User {
private var id : Int = 0
private var username : String=""
private var secretKey : String=""
private var issuer : String=""
private var code : String=""
constructor(id: Int, username: String, secretKey: String, issuer: String) {
this.id = id
this.username = username
this.secretKey = secretKey
this.issuer = issuer
}
fun getId() : Int {
return id
}
fun setId(id : Int) {
this.id = id
}
fun getUsername() : String {
return username
}
fun setUsername(username : String) {
this.username = username
}
fun getSecretKey() : String {
return secretKey
}
fun setSecretKey(secretKey : String) {
this.secretKey = secretKey
}
fun getCode() : String {
return code
}
fun setCode(code : String) {
this.code = code
}
fun getIssuer() : String {
return issuer
}
fun setIssuer(issuer : String) {
this.issuer = issuer
}
}
CodeListAdapter
作用:适配列表内每一个数据,为令牌动态计算提供适配
package com.shixiaotian.totp.scan.application
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import com.shixiaotian.totp.scan.application.tools.EncodeTools
import com.shixiaotian.totp.scan.application.vo.User
/**
* 动态码列表内容适配器
*/
class CodeListAdapter (context: Context, val resourceId: Int, data: List<User>) : ArrayAdapter<User>(context, resourceId, data) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = LayoutInflater.from(context).inflate(resourceId, parent, false)
val userId: TextView = view.findViewById(R.id.user_id)
val issuer: TextView = view.findViewById(R.id.user_issuer)
val username: TextView = view.findViewById(R.id.user_username)
val userSecretKey: TextView = view.findViewById(R.id.user_secretKey)
val userCode: TextView = view.findViewById(R.id.user_code)
val user = getItem(position)
if (user!=null){
userId.text = user.getId().toString()
issuer.text = user.getIssuer()
username.text = user.getUsername()
userSecretKey.text = user.getSecretKey()
var code = EncodeTools.encode(user.getSecretKey()) as String
user.setCode(code)
userCode.text = user.getCode()
}
return view
}
}
MainActivity
作用:添加主页面上基本的按钮监听
package com.shixiaotian.totp.scan.application
import android.os.Bundle
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.FragmentManager
import com.shixiaotian.totp.scan.application.fragments.CodeAddFragment
import com.shixiaotian.totp.scan.application.fragments.CodeListFragment
import com.shixiaotian.totp.scan.application.db.DatabaseHelper
import com.shixiaotian.totp.scan.application.tools.FirstRunTools
import net.sqlcipher.database.SQLiteDatabase
class MainActivity : AppCompatActivity() {
private lateinit var listButton: View
private lateinit var addButton: View
private lateinit var fragmentManager: FragmentManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
// 初始化预处理
init()
val codeAddFragment = CodeAddFragment()
val codeListFragment = CodeListFragment()
fragmentManager = supportFragmentManager
fragmentManager.beginTransaction().replace(R.id.viewPager, codeListFragment).commit()
// 菜单按钮监听
listButton = findViewById(R.id.menuButton)
listButton.setOnClickListener {
fragmentManager.beginTransaction().replace(R.id.viewPager, codeListFragment).commit()
}
// 添加按钮监听
addButton = findViewById(R.id.addButton)
addButton.setOnClickListener {
fragmentManager.beginTransaction().replace(R.id.viewPager, codeAddFragment).commit()
}
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
}
private fun init() {
SQLiteDatabase.loadLibs(this);
println("---开始初始化")
// 判断是否首次运行
if(FirstRunTools.isFirstRun(this)) {
println("---首次运行触发")
val dbHelper = DatabaseHelper(this)
// 初始化数据库
dbHelper.initDB()
dbHelper.init()
}
}
}
activity_main.xml
作用:主页面布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#3F5CB5"
tools:context=".MainActivity">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/mainFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
</FrameLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="200px"
android:layout_alignParentBottom="true"
android:background="#ffffff"
android:orientation="horizontal">
<ImageView
android:id="@+id/menuButton"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#20212E"
android:gravity="center_horizontal"
android:src="@android:drawable/ic_menu_search"
android:layout_marginRight="5px"
android:textSize="30sp" />
<ImageView
android:id="@+id/addButton"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#20212E"
android:gravity="center_horizontal"
android:src="@android:drawable/ic_menu_add"
android:layout_marginLeft="5px"
android:textSize="30sp" />
</LinearLayout>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
code_item.xml
作用:令牌列表每一个令牌的布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/User"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:id="@+id/user_id"
android:layout_width="0px"
android:layout_height="0px"
android:layout_gravity="left"
android:visibility="invisible"
/>
<TextView
android:id="@+id/user_secretKey"
android:layout_width="0px"
android:layout_height="0px"
android:layout_gravity="center_vertical"
android:visibility="invisible"
/>
<TextView
android:id="@+id/user_issuer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceListItemSmall"
android:gravity="center_vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:textSize="35sp"
android:text="apple"
/>
<TextView
android:id="@+id/user_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceListItemSmall"
android:gravity="center_vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:textSize="25sp"
android:text="1234567@qq.com"
/>
<TextView
android:id="@+id/user_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:layout_gravity="center"
android:textColor="#000000"
android:text="665277"
android:textSize="55sp"
android:textStyle="bold"
/>
</LinearLayout>
</LinearLayout>
fragment_code_add.xml
作用:添加令牌页面,手动添加或者,触发zxing扫码添加
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
tools:context=".fragments.CodeAddFragment">
<!-- TODO: Update blank fragment layout -->
<LinearLayout
android:layout_margin="100px"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/addNameView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="40sp"
android:textStyle="bold"
android:text="Username" />
<EditText
android:id="@+id/addUsernameText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLength="10"
android:inputType="text"
android:textSize="30sp"
/>
<TextView
android:id="@+id/addSecretKeyView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="40sp"
android:textStyle="bold"
android:text="SecretKey" />
<EditText
android:id="@+id/addSecretKeyText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLength="100"
android:textSize="30sp"
android:inputType="text" />
<TextView
android:id="@+id/addIssuerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="40sp"
android:textStyle="bold"
android:text="Issuer" />
<EditText
android:id="@+id/addIssuerText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLength="10"
android:inputType="text"
android:textSize="30sp"
/>
<TextView
android:id="@+id/saveButton"
android:layout_width="match_parent"
android:layout_height="100sp"
android:background="#A62641"
android:gravity="center"
android:layout_marginTop="100px"
android:textColor="#ffffff"
android:text="Save"
android:textSize="50sp" />
<TextView
android:id="@+id/cameraButton"
android:layout_width="match_parent"
android:layout_height="100sp"
android:layout_marginTop="100px"
android:background="#20212E"
android:gravity="center"
android:text="Scan"
android:textColor="#ffffff"
android:textSize="50sp" />
</LinearLayout>
</FrameLayout>
fragment_code_list.xml
作用:提供令牌快速查看列表,和选择单个令牌进行操作的功能
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/code_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.CodeListFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/textView3"
android:layout_width="match_parent"
android:layout_height="101dp"
android:gravity="center"
android:background="#312F2F"
android:text="30s"
android:textColor="#ffffff"
android:textColorLink="#FFFFFF"
android:textSize="60sp" />
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="#ffffff"
android:divider="#000000"
android:dividerHeight="1dp" />
<View
android:layout_width="match_parent"
android:layout_height="200px"
android:background="#666666"
>
</View>
</LinearLayout>
</FrameLayout>
fragment_code_show.xml
作用:动态令牌展示页面,提供令牌查看和删除功能
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/code_show"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#312F2F"
tools:context=".fragments.CodeShowFragment">
<!-- TODO: Update blank fragment layout -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/showIssuerTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:layout_marginTop="100px"
android:text="steam"
android:textColor="#ffffff"
android:textColorLink="#FFFFFF"
android:textSize="75sp" />
<TextView
android:id="@+id/showUsernameTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:layout_marginTop="30px"
android:text="54526322@qq.com"
android:textColor="#ffffff"
android:textColorLink="#FFFFFF"
android:textSize="25sp" />
<TextView
android:id="@+id/showTimeView"
android:layout_width="match_parent"
android:layout_height="101dp"
android:gravity="center"
android:background="#312F2F"
android:text="30s"
android:textColor="#ffffff"
android:textColorLink="#FFFFFF"
android:layout_marginTop="20px"
android:textSize="70sp" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="45dp"
android:max="100"
android:progress="10"
android:progressDrawable="@drawable/progress_bar_color" />
<TextView
android:id="@+id/showCodeView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:freezesText="false"
android:gravity="center_horizontal"
android:layout_marginTop="50px"
android:text="9TXTSY"
android:textColor="#ffffff"
android:textColorLink="#FFFFFF"
android:textSize="80sp" />
<TextView
android:id="@+id/deleteButton"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="100sp"
android:background="#20212E"
android:gravity="center"
android:layout_margin="50px"
android:textColor="#ffffff"
android:text="Delete"
android:textSize="50sp" />
</LinearLayout>
</FrameLayout>
AndroidManifest.xml
作用:添加相机权限,设定zxing相机扫码activity相机方向等相关数据
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@android:drawable/ic_lock_lock"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyApplication"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor"
tools:replace="screenOrientation" />
</application>
</manifest>