Compare commits

..

2 Commits

Author SHA1 Message Date
057e4bfe3a feat(index): 添加后退按键自定义处理逻辑
- 在首页组件中添加 onBackPress 方法
- 当详情页显示时,后退键关闭详情页并返回列表
- 否则,执行系统默认后退行为(退出应用)
2025-08-18 15:15:05 +08:00
2786542ffd feat(totp): 重构应用以支持手机端管理账户
- 移除本地账户配置文件,改为从本地存储加载账户
- 新增 interconnect 功能,实现与手机端的数据同步
- 支持从手机端接收、删除和重排序账户
- 增加删除账户和重排序的确认消息发送到手机端
- 优化 TOTP 生成算法,支持 SHA256 和 SHA512
- 调整 OTP 生成位数,支持 6 位以上 OTP
2025-08-18 13:33:43 +08:00
5 changed files with 444 additions and 85 deletions

4
package-lock.json generated
View File

@ -1,11 +1,11 @@
{
"name": "hello",
"name": "totp",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hello",
"name": "totp",
"version": "1.0.0",
"dependencies": {
"crypto-js": "^4.2.0"

View File

@ -1,44 +0,0 @@
{
"accounts": [
{
"name": "Microsoft",
"username": "example@outlook.com",
"secret": "JBSWY3DPEHPK3PXP",
"enabled": true,
"type": "totp"
},
{
"name": "GitHub",
"username": "myusername",
"secret": "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ",
"enabled": true,
"type": "totp"
},
{
"name": "Steam",
"username": "steamuser",
"secret": "NBSWY3DPEHPK3PXPNBSWY3DPEHPK3PXP",
"enabled": true,
"type": "steam"
},
{
"name": "Work Server",
"username": "employee",
"secret": "MFRGG2LTEBYGCZLSMUFA====",
"enabled": true,
"type": "totp"
},
{
"name": "Microsoft",
"username": "user@example.com",
"secret": "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ",
"enabled": false,
"type": "totp"
}
],
"settings": {
"title": "TOTP",
"refreshInterval": 20,
"maxDisplayAccounts": 10
}
}

View File

@ -1,8 +1,8 @@
{
"package": "cn.deepfal.band.TOTPauthenticator",
"name": "TOTP",
"versionName": "1.0.2",
"versionCode": 1,
"versionName": "1.1.0",
"versionCode": 2,
"minPlatformVersion": 1000,
"icon": "/common/logo.png",
"deviceTypeList": [
@ -14,6 +14,12 @@
},
{
"name": "system.vibrator"
},
{
"name": "system.interconnect"
},
{
"name": "system.storage"
}
],
"config": {

View File

@ -86,16 +86,20 @@
<script>
import { TOTP, SteamTotp } from '../../utils/totp'
import accountsConfig from '../../common/accounts.json'
import vibrator from '@system.vibrator'
import storage from '@system.storage'
import interconnect from '@system.interconnect'
// Key class similar to BandTOTP
class TOTPKey {
constructor(key, name, usr = '', type = 'totp') {
constructor(key, name, usr = '', type = 'totp', algorithm = 'SHA1', digits = 6, period = 30) {
this.name = name
this.usr = usr
this.key = key
this.type = type // 新增类型字段:'totp' 或 'steam'
this.type = type // 'totp' 或 'steam'
this.algorithm = algorithm
this.digits = digits
this.period = period
this.update()
}
@ -105,7 +109,7 @@ class TOTPKey {
if (this.type === 'steam') {
this.otp = SteamTotp(this.key)
} else {
this.otp = TOTP(this.key)
this.otp = TOTP(this.key, this.algorithm, this.digits, this.period)
}
} catch (error) {
this.otp = "ERROR"
@ -113,6 +117,8 @@ class TOTPKey {
}
}
let interconnectConn = null
export default {
private: {
totpList: [],
@ -130,59 +136,417 @@ export default {
lastUpdateTime: 0,
// 配置相关
appTitle: "TOTP验证器",
refreshInterval: 100
refreshInterval: 100,
// interconnect状态
isConnected: false,
connectionStatus: '未连接'
},
onInit() {
console.log('TOTP应用初始化')
// 加载配置文件
this.loadAccountsFromConfig()
// 初始化interconnect连接
this.initInterconnect()
// 只从本地存储加载账户(不再从配置文件加载)
this.loadAccountsFromStorage()
// 设置interconnect消息监听
this.setupInterconnectListener()
// 设置初始颜色
this.updateColors(this.timeLeft)
this.startTimer()
},
loadAccountsFromConfig() {
initInterconnect() {
console.log('初始化interconnect连接')
try {
interconnectConn = interconnect.instance()
// 加载设置
if (accountsConfig.settings) {
this.appTitle = accountsConfig.settings.title || "TOTP验证器"
this.refreshInterval = accountsConfig.settings.refreshInterval || 100
interconnectConn.getReadyState({
success: data => {
console.log('interconnect状态:', data.status)
if (data.status === 1) {
this.isConnected = true
this.connectionStatus = '已连接'
console.log('interconnect连接成功')
} else if (data.status === 2) {
this.isConnected = false
this.connectionStatus = '连接失败'
console.log('interconnect连接失败')
}
},
fail: (data, code) => {
console.log('获取interconnect状态失败, code =', code)
this.connectionStatus = '状态检查失败'
}
})
// 在initInterconnect中不设置onmessage使用单独的方法
// 监听消息接收将在setupInterconnectListener中设置
// 监听连接打开
interconnectConn.onopen = () => {
console.log('interconnect连接已打开')
this.isConnected = true
this.connectionStatus = '已连接'
// 连接成功后,请求当前账户列表给手机
this.sendCurrentAccountsToPhone()
}
// 监听连接关闭
interconnectConn.onclose = (data) => {
console.log('interconnect连接已关闭, reason =', data.data, ', code =', data.code)
this.isConnected = false
this.connectionStatus = '连接已关闭'
}
// 监听连接错误
interconnectConn.onerror = (data) => {
console.log('interconnect连接错误, errMsg =', data.data, ', errCode =', data.code)
this.isConnected = false
this.connectionStatus = '连接错误'
}
} catch (error) {
console.log('初始化interconnect失败:', error)
this.connectionStatus = '初始化失败'
}
},
setupInterconnectListener() {
if (!interconnectConn) {
console.log('interconnect连接不存在无法设置监听')
return
}
// 完全模仿BandTOTP-Band的消息处理逻辑
interconnectConn.onmessage = (data) => {
try {
console.log('接收到手机消息:', data.data)
let parsedData = JSON.parse(data.data)
if (parsedData.list && Array.isArray(parsedData.list)) {
parsedData.list.forEach((e) => {
// 查找是否已存在相同密钥
let index = this.totpList.findIndex((a) => e.key == a.key)
if (index != -1) {
// 密钥重复:只更新名字和用户名
this.totpList[index].name = e.name
this.totpList[index].usr = e.usr
} else {
// 新密钥:创建新实例并添加
this.totpList.push(new TOTPKey(
e.key,
e.name,
e.usr || '',
e.type || 'totp',
e.algorithm || 'SHA1',
e.digits || 6,
e.period || 30
))
}
})
// 关键立即保存到存储学习BandTOTP-Band
this.saveAccountsToStorage()
console.log('成功更新', this.totpList.length, '个账户')
// 震动反馈表示数据同步成功
vibrator.vibrate({
mode: 'short'
})
setTimeout(() => {
vibrator.vibrate({
mode: 'short'
})
}, 100)
}
// 处理获取账户列表请求
if (parsedData.cmd === 'getAccounts') {
console.log('手机请求获取账户列表')
this.sendCurrentAccountsToPhone()
}
// 处理删除账户请求
if (parsedData.cmd === 'deleteAccount' && parsedData.key) {
console.log('手机请求删除账户:', parsedData.key)
this.deleteAccountByKey(parsedData.key)
}
// 处理重排序账户请求
if (parsedData.cmd === 'reorderAccounts' && parsedData.accounts) {
console.log('手机请求重排序账户')
this.reorderAccounts(parsedData.accounts)
}
} catch (error) {
console.log('处理手机消息失败:', error)
}
}
},
sendCurrentAccountsToPhone() {
if (!interconnectConn || !this.isConnected) {
console.log('interconnect未连接无法发送账户列表')
return
}
try {
// 构造与Android app兼容的数据格式
const accountsData = {
list: this.totpList.map(account => ({
name: account.name,
usr: account.usr,
key: account.key,
algorithm: account.algorithm || 'SHA1',
digits: account.digits || 6,
period: account.period || 30,
type: account.type || 'totp'
}))
}
// 加载启用的账户
const enabledAccounts = accountsConfig.accounts.filter(account => account.enabled)
console.log('发送账户列表到手机:', accountsData)
// 转换为TOTPKey实例
this.totpList = enabledAccounts.map(account => {
return new TOTPKey(
account.secret,
account.name,
account.username,
account.type || 'totp' // 如果没有指定type默认为totp
)
interconnectConn.send({
data: accountsData,
success: () => {
console.log('账户列表发送成功')
},
fail: (data) => {
console.log('账户列表发送失败, errMsg =', data.data, ', errCode =', data.code)
}
})
} catch (error) {
console.log('构造发送数据失败:', error)
}
},
loadAccountsFromStorage() {
storage.get({
key: 'keys',
success: (data) => {
if (data) {
console.log('从存储加载账户:', data)
try {
const parsedData = JSON.parse(data)
if (parsedData && parsedData.list && Array.isArray(parsedData.list)) {
// 清空现有列表,避免重复加载
this.totpList = []
parsedData.list.forEach((account) => {
this.totpList.push(new TOTPKey(
account.key,
account.name,
account.usr || '',
account.type || 'totp',
account.algorithm || 'SHA1',
account.digits || 6,
account.period || 30
))
})
console.log('从存储加载了', this.totpList.length, '个账户')
}
} catch (error) {
console.log('解析存储数据失败:', error)
}
} else {
// 如果没有存储数据,确保列表为空
console.log('存储中没有账户数据')
this.totpList = []
}
},
fail: (data, code) => {
console.log('从存储加载账户失败:', code)
// 加载失败时也确保列表为空,避免使用旧数据
this.totpList = []
}
})
},
saveAccountsToStorage() {
const saveData = {
list: this.totpList.map(account => ({
name: account.name,
usr: account.usr,
key: account.key,
algorithm: account.algorithm || 'SHA1',
digits: account.digits || 6,
period: account.period || 30,
type: account.type || 'totp'
}))
}
storage.set({
key: 'keys',
value: JSON.stringify(saveData)
})
console.log('账户保存到存储,数量:', this.totpList.length)
},
deleteAccountByKey(keyToDelete) {
const initialCount = this.totpList.length
// 根据key删除账户
this.totpList = this.totpList.filter(account => account.key !== keyToDelete)
const deletedCount = initialCount - this.totpList.length
if (deletedCount > 0) {
// 保存更新后的账户列表
this.saveAccountsToStorage()
console.log('成功删除账户,删除数量:', deletedCount, '剩余数量:', this.totpList.length)
// 删除成功震动反馈
vibrator.vibrate({
mode: 'long'
})
this.totpList.forEach(() => {
// 如果正在查看被删除的账户详情,关闭详情页
if (this.showDetailPage && this.selectedItem && this.selectedItem.key === keyToDelete) {
this.hideDetail()
}
// 向手机发送删除成功确认
this.sendDeleteConfirmationToPhone(keyToDelete, true)
} else {
console.log('未找到要删除的账户:', keyToDelete)
// 向手机发送删除失败确认
this.sendDeleteConfirmationToPhone(keyToDelete, false)
}
},
sendDeleteConfirmationToPhone(deletedKey, success) {
if (!interconnectConn || !this.isConnected) {
console.log('interconnect未连接无法发送删除确认')
return
}
try {
const confirmationData = {
cmd: 'deleteConfirmation',
key: deletedKey,
success: success
}
console.log('发送删除确认到手机:', confirmationData)
interconnectConn.send({
data: confirmationData,
success: () => {
console.log('删除确认发送成功')
},
fail: (data) => {
console.log('删除确认发送失败, errMsg =', data.data, ', errCode =', data.code)
}
})
} catch (error) {
console.log('构造删除确认数据失败:', error)
}
},
reorderAccounts(newAccounts) {
try {
console.log('开始重排序账户,新顺序数量:', newAccounts.length)
// 根据新顺序重建账户列表
const reorderedList = []
newAccounts.forEach((accountData) => {
// 在现有列表中查找匹配的账户
const existingAccount = this.totpList.find(account => account.key === accountData.key)
if (existingAccount) {
// 保持现有的TOTPKey实例只更新顺序
reorderedList.push(existingAccount)
} else {
// 如果没找到创建新的TOTPKey实例
console.log('创建新账户实例:', accountData.name)
reorderedList.push(new TOTPKey(
accountData.key,
accountData.name,
accountData.usr || '',
accountData.type || 'totp',
accountData.algorithm || 'SHA1',
accountData.digits || 6,
accountData.period || 30
))
}
})
// 更新账户列表
this.totpList = reorderedList
// 保存到存储
this.saveAccountsToStorage()
console.log('账户重排序完成,新顺序数量:', this.totpList.length)
// 重排序成功震动反馈
vibrator.vibrate({
mode: 'short'
})
setTimeout(() => {
vibrator.vibrate({
mode: 'short'
})
}, 100)
// 向手机发送重排序成功确认
this.sendReorderConfirmationToPhone(true)
} catch (error) {
console.error('Failed to load accounts config:', error)
// 如果加载配置失败,使用默认账户
this.loadDefaultAccounts()
console.log('重排序账户失败:', error)
this.sendReorderConfirmationToPhone(false)
}
},
sendReorderConfirmationToPhone(success) {
if (!interconnectConn || !this.isConnected) {
console.log('interconnect未连接无法发送重排序确认')
return
}
try {
const confirmationData = {
cmd: 'reorderConfirmation',
success: success
}
console.log('发送重排序确认到手机:', confirmationData)
interconnectConn.send({
data: confirmationData,
success: () => {
console.log('重排序确认发送成功')
},
fail: (data) => {
console.log('重排序确认发送失败, errMsg =', data.data, ', errCode =', data.code)
}
})
} catch (error) {
console.log('构造重排序确认数据失败:', error)
}
},
loadDefaultAccounts() {
},
// 已移除loadAccountsFromConfig - 只能通过手机端管理账户
onShow() {
this.isVisible = true
this.updateCodes()
this.startTimer()
// 页面显示时,如果已连接则发送当前账户列表
if (this.isConnected) {
this.sendCurrentAccountsToPhone()
}
},
onHide() {
@ -206,6 +570,15 @@ export default {
clearInterval(this.updateInterval)
this.updateInterval = null
}
// 清理interconnect连接
if (interconnectConn) {
try {
interconnectConn = null
} catch (error) {
console.log('清理interconnect连接失败:', error)
}
}
},
updateCodes() {
@ -343,6 +716,17 @@ export default {
vibrator.vibrate({
mode: 'short'
})
},
onBackPress() {
console.log('触发onBackPress')
// 如果详情页打开,则关闭详情页返回列表
if (this.showDetailPage) {
this.hideDetail()
return true // 自己处理,不执行系统默认返回
}
// 否则执行系统默认返回行为(退出应用)
return false
}
}

View File

@ -3,9 +3,9 @@ let timeOffset=0
function time(){
return Math.floor(Date.now() / 1000) + (timeOffset || 0);
}
function TOTP(key) {
function TOTP(key, algorithm = 'SHA1', digits = 6, period = 30) {
const timeStep = 30; // 默认时间步长为 30 秒
const timeStep = period; // 使用传入的时间周期
const currentTime = time()
const timeCounter = Math.floor(currentTime / timeStep).toString(16).padStart(16, '0'); // 时间计数器16位
@ -13,8 +13,20 @@ function TOTP(key) {
// 使用 Base32 解码并返回 WordArray
const decodedKey = base32Decode(key);
// 使用 HMAC-SHA1 计算哈希值
const hmacHash = CryptoJS.HmacSHA1(CryptoJS.enc.Hex.parse(timeCounter),decodedKey);
// 根据算法选择不同的HMAC函数
let hmacHash;
switch (algorithm.toUpperCase()) {
case 'SHA256':
hmacHash = CryptoJS.HmacSHA256(CryptoJS.enc.Hex.parse(timeCounter), decodedKey);
break;
case 'SHA512':
hmacHash = CryptoJS.HmacSHA512(CryptoJS.enc.Hex.parse(timeCounter), decodedKey);
break;
case 'SHA1':
default:
hmacHash = CryptoJS.HmacSHA1(CryptoJS.enc.Hex.parse(timeCounter), decodedKey);
break;
}
// 动态截取
const hmacBytes = hexToBytes(hmacHash.toString());
@ -25,8 +37,9 @@ function TOTP(key) {
(hmacBytes[offset + 2] << 8) |
hmacBytes[offset + 3];
// 生成 6 位 OTP
const result = (otp % 1000000).toString().padStart(6, '0');
// 生成指定位数的OTP
const modulo = Math.pow(10, digits);
const result = (otp % modulo).toString().padStart(digits, '0');
return result
} catch (error) {
throw error
@ -77,7 +90,7 @@ function bufferizeSecret(secret) {
if (secret.match(/^[0-9a-f]{40}$/i)) {
return CryptoJS.enc.Hex.parse(secret); // 解析为 WordArray
} else {
// 假设是 base32 编码
// 假设是 base32 编码包括Steam的标准secret
return base32Decode(secret); // 解析为 WordArray
}
}